diff --git a/.changes/0.2.4.md b/.changes/0.2.4.md new file mode 100644 index 0000000..26a1a45 --- /dev/null +++ b/.changes/0.2.4.md @@ -0,0 +1,9 @@ +## 0.2.4 - 2023-02-18 +### Added +* Notifications now display errors and the output of the failed command. +* CI configs for GitHub and Woodpecker +* Added `version` subcommand +### Changed +* Console logging can be disabled by setting `console-disabled` in the `logging` object +## Fixed +* If Host was not defined for an incomplete `hosts` object, any commands would fail as they could not look up the values in the SSH config files. diff --git a/.changes/header.tpl.md b/.changes/header.tpl.md new file mode 100644 index 0000000..df8faa7 --- /dev/null +++ b/.changes/header.tpl.md @@ -0,0 +1,6 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), +and is generated by [Changie](https://github.com/miniscruff/changie). diff --git a/.changes/unreleased/.gitkeep b/.changes/unreleased/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.changie.yaml b/.changie.yaml new file mode 100644 index 0000000..906d495 --- /dev/null +++ b/.changie.yaml @@ -0,0 +1,26 @@ +changesDir: .changes +unreleasedDir: unreleased +headerPath: header.tpl.md +changelogPath: CHANGELOG.md +versionExt: md +versionFormat: '## {{.Version}} - {{.Time.Format "2006-01-02"}}' +kindFormat: '### {{.Kind}}' +changeFormat: '* {{.Body}}' +kinds: +- label: Added + auto: minor +- label: Changed + auto: major +- label: Deprecated + auto: minor +- label: Removed + auto: major +- label: Fixed + auto: patch +- label: Security + auto: patch +newlines: + afterChangelogHeader: 1 + beforeChangelogVersion: 1 + endOfVersion: 1 +envPrefix: CHANGIE_ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..2c19b23 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,37 @@ +name: goreleaser + +on: + push: + branches: [ master ] # your default branch if different + paths: [ CHANGELOG.md ] # your changelog file if different + +permissions: + contents: write + packages: write + # issues: write + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - run: git fetch --force --tags + - uses: actions/setup-go@v3 + with: + go-version: '==1.19.5' + cache: true + # More assembly might be required: Docker logins, GPG, etc. It all depends + # on your needs. + - uses: goreleaser/goreleaser-action@v4 + with: + # either 'goreleaser' (default) or 'goreleaser-pro': + distribution: goreleaser + version: latest + args: release --release-notes=".changes/$(go run backy.go version)" -f .goreleaser/github.yml --clean + env: + GITHUB_TOKEN: ${{ secrets.GORELEASER_TOKEN }} + # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' + # distribution: + # GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} diff --git a/.goreleaser.yaml b/.goreleaser/gitea.yml similarity index 81% rename from .goreleaser.yaml rename to .goreleaser/gitea.yml index 7acf28a..3040123 100644 --- a/.goreleaser.yaml +++ b/.goreleaser/gitea.yml @@ -1,5 +1,3 @@ -# This is an example .goreleaser.yml file with some sensible defaults. -# Make sure to check the documentation at https://goreleaser.com before: hooks: # You may remove this if you don't use go modules. @@ -19,7 +17,7 @@ archives: - format: tar.gz # this name template makes the OS and Arch compatible with the results of uname. name_template: >- - {{ .ProjectName }} + {{ .ProjectName }}_{{ .Version }}_ {{- title .Os }}_ {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 @@ -34,11 +32,7 @@ checksum: snapshot: name_template: "{{ incpatch .Version }}-next" changelog: - sort: asc - filters: - exclude: - - '^docs:' - - '^test:' + skip: false gitea_urls: api: https://git.andrewnw.xyz/api/v1 diff --git a/.goreleaser/github.yml b/.goreleaser/github.yml new file mode 100644 index 0000000..53aca5b --- /dev/null +++ b/.goreleaser/github.yml @@ -0,0 +1,41 @@ +# This is an example .goreleaser.yml file with some sensible defaults. +# Make sure to check the documentation at https://goreleaser.com +before: + hooks: + # You may remove this if you don't use go modules. + - go mod tidy +builds: + - env: + - CGO_ENABLED=0 + goos: + - freebsd + - linux + goarch: + - "386" + - amd64 + - arm64 + +archives: + - format: tar.gz + # this name template makes the OS and Arch compatible with the results of uname. + name_template: >- + {{ .ProjectName }}_{{ .Version }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + # use zip for windows archives + format_overrides: + - goos: windows + format: zip +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ incpatch .Version }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' \ No newline at end of file diff --git a/.goreleaser/vern.yml b/.goreleaser/vern.yml new file mode 100644 index 0000000..78a148f --- /dev/null +++ b/.goreleaser/vern.yml @@ -0,0 +1,46 @@ +before: + hooks: + # You may remove this if you don't use go modules. + - go mod tidy +builds: + - env: + - CGO_ENABLED=0 + goos: + - freebsd + - linux + goarch: + - "386" + - amd64 + - arm64 + +archives: + - format: binary + # this name template makes the OS and Arch compatible with the results of uname. + name_template: >- + {{ .ProjectName }}_{{ .Version }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + # use zip for windows archives + format_overrides: + - goos: windows + format: zip +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ incpatch .Version }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' + +gitea_urls: + api: https://git.vern.cc/api/v1 + download: https://git.vern.cc +# The lines beneath this are called `modelines`. See `:help modeline` +# Feel free to remove those if you don't want/use them. +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json diff --git a/.woodpecker/vern.yml b/.woodpecker/vern.yml new file mode 100644 index 0000000..fde8f4c --- /dev/null +++ b/.woodpecker/vern.yml @@ -0,0 +1,10 @@ +pipeline: + release: + image: goreleaser/goreleaser + commands: + - goreleaser release -f .goreleaser/vern.yml --release-notes=".changes/$(go run backy.go version)" + secrets: [ gitea_token ] + when: + event: tag + +branches: master \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ffbd163 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), +and is generated by [Changie](https://github.com/miniscruff/changie). + + +## 0.2.4 - 2023-02-18 +### Added +* Notifications now display errors and the output of the failed command. +* CI configs for GitHub and Woodpecker +* Added `version` subcommand +### Changed +* Console logging can be disabled by setting `console-disabled` in the `logging` object +## Fixed +* If Host was not defined for an incomplete `hosts` object, any commands would fail as they could not look up the values in the SSH config files. diff --git a/Makefile b/Makefile index a3f9da8..4bb9e74 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,8 @@ build: go build -gorealeaser-build: - goreleaser release --snapshot --rm-dist \ No newline at end of file +install: + go install . + +goreleaser-snapshot: + goreleaser -f .goreleaser/gitea.yml release --snapshot --clean \ No newline at end of file diff --git a/cmd/backup.go b/cmd/backup.go index dfeda08..8f20526 100644 --- a/cmd/backup.go +++ b/cmd/backup.go @@ -12,11 +12,10 @@ import ( var ( backupCmd = &cobra.Command{ - Use: "backup [--lists==list1,list2]", + Use: "backup [--lists=list1,list2]", Short: "Runs commands defined in config file.", - Long: `Backup executes commands defined in config file. - Use the --lists flag to execute the specified commands.`, - Run: Backup, + Long: "Backup executes commands defined in config file.\nUse the --lists flag to execute the specified commands. If not specified, all lists will be executed.", + Run: Backup, } ) diff --git a/cmd/cron.go b/cmd/cron.go index 143f3e8..d9ad955 100644 --- a/cmd/cron.go +++ b/cmd/cron.go @@ -8,9 +8,9 @@ import ( var ( cronCmd = &cobra.Command{ - Use: "cron command ...", - Short: "Runs commands defined in config file.", - Long: `Cron executes commands at the time defined in config file.`, + Use: "cron [flags]", + Short: "Runs command lists defined in config file.", + Long: `Cron starts a scheduler that executes command lists at the time defined in config file.`, Run: cron, } ) diff --git a/cmd/exec.go b/cmd/exec.go index 49ea8c8..e5b9934 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -29,6 +29,5 @@ func execute(cmd *cobra.Command, args []string) { opts := backy.NewOpts(cfgFile, backy.AddCommands(args)) opts.InitConfig() // opts.InitMongo() - backy.ReadConfig(opts).ExecuteCmds() - + backy.ReadConfig(opts).ExecuteCmds(opts) } diff --git a/cmd/root.go b/cmd/root.go index fbca535..0a4dc13 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -36,5 +36,5 @@ func init() { rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "f", "", "config file to read from") rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Sets verbose level") - rootCmd.AddCommand(backupCmd, execCmd, cronCmd) + rootCmd.AddCommand(backupCmd, execCmd, cronCmd, versionCmd) } diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..0152877 --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +const versionStr = "0.2.4" + +var ( + versionCmd = &cobra.Command{ + Use: "version", + Short: "Prints the version and exits.", + Run: version, + } +) + +func version(cmd *cobra.Command, args []string) { + + fmt.Printf("v%s\n", versionStr) + + os.Exit(0) +} diff --git a/pkg/backy/backy.go b/pkg/backy/backy.go index 540c373..59e26c0 100644 --- a/pkg/backy/backy.go +++ b/pkg/backy/backy.go @@ -29,9 +29,10 @@ 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. -func (command *Command) RunCmd(log *zerolog.Logger, hosts map[string]*Host) error { +func (command *Command) RunCmd(log *zerolog.Logger, hosts map[string]*Host) ([]string, error) { var ( + outputArr []string ArgsStr string cmdOutBuf bytes.Buffer cmdOutWriters io.Writer @@ -41,24 +42,23 @@ func (command *Command) RunCmd(log *zerolog.Logger, hosts map[string]*Host) erro env: command.Environment, } ) - envVars.env = append(envVars.env, os.Environ()...) for _, v := range command.Args { ArgsStr += fmt.Sprintf(" %s", v) } if command.Host != nil { - log.Info().Str("Command", fmt.Sprintf("Running command: %s %s on host %s", command.Cmd, ArgsStr, *command.Host)).Send() + log.Info().Str("Command", fmt.Sprintf("Running command %s %s on host %s", command.Cmd, ArgsStr, *command.Host)).Send() err := command.RemoteHost.ConnectToSSHHost(log, hosts) if err != nil { - return err + return nil, err } defer command.RemoteHost.SshClient.Close() commandSession, err := command.RemoteHost.SshClient.NewSession() if err != nil { log.Err(fmt.Errorf("new ssh session: %w", err)).Send() - return err + return nil, err } defer commandSession.Close() @@ -81,12 +81,15 @@ func (command *Command) RunCmd(log *zerolog.Logger, hosts map[string]*Host) erro outMap := make(map[string]interface{}) outMap["cmd"] = cmd outMap["output"] = outScanner.Text() + if str, ok := outMap["output"].(string); ok { + outputArr = append(outputArr, str) + } log.Info().Fields(outMap).Send() } if err != nil { log.Error().Err(fmt.Errorf("error when running cmd: %s: %w", command.Cmd, err)).Send() - return err + return outputArr, err } } else { cmdExists := command.checkCmdExists() @@ -96,7 +99,7 @@ func (command *Command) RunCmd(log *zerolog.Logger, hosts map[string]*Host) erro var err error if command.Shell != "" { - log.Info().Str("Command", fmt.Sprintf("Running command: %s %s on local machine in %s", command.Cmd, ArgsStr, command.Shell)).Send() + log.Info().Str("Command", fmt.Sprintf("Running command %s %s on local machine in %s", command.Cmd, ArgsStr, command.Shell)).Send() ArgsStr = fmt.Sprintf("%s %s", command.Cmd, ArgsStr) localCMD := exec.Command(command.Shell, "-c", ArgsStr) if command.Dir != nil { @@ -118,23 +121,29 @@ func (command *Command) RunCmd(log *zerolog.Logger, hosts map[string]*Host) erro outMap := make(map[string]interface{}) outMap["cmd"] = command.Cmd outMap["output"] = outScanner.Text() + if str, ok := outMap["output"].(string); ok { + outputArr = append(outputArr, str) + } log.Info().Fields(outMap).Send() } if err != nil { - log.Error().Err(fmt.Errorf("error when running cmd: %s: %w", command.Cmd, err)).Send() - return err + log.Error().Err(fmt.Errorf("error when running cmd %s: %w", command.Cmd, err)).Send() + return outputArr, err } - return nil + return outputArr, nil } - log.Info().Str("Command", fmt.Sprintf("Running command: %s %s on local machine", command.Cmd, ArgsStr)).Send() + log.Info().Str("Command", fmt.Sprintf("Running command %s %s on local machine", command.Cmd, ArgsStr)).Send() localCMD := exec.Command(command.Cmd, command.Args...) if command.Dir != nil { localCMD.Dir = *command.Dir } + // fmt.Printf("%v\n", envVars.env) + injectEnvIntoLocalCMD(envVars, localCMD, log) cmdOutWriters = io.MultiWriter(&cmdOutBuf) + // fmt.Printf("%v\n", localCMD.Environ()) if IsCmdStdOutEnabled() { cmdOutWriters = io.MultiWriter(os.Stdout, &cmdOutBuf) @@ -147,17 +156,21 @@ func (command *Command) RunCmd(log *zerolog.Logger, hosts map[string]*Host) erro outMap := make(map[string]interface{}) outMap["cmd"] = command.Cmd outMap["output"] = outScanner.Text() + if str, ok := outMap["output"].(string); ok { + outputArr = append(outputArr, str) + } log.Info().Fields(outMap).Send() } if err != nil { - log.Error().Err(fmt.Errorf("error when running cmd: %s: %w", command.Cmd, err)).Send() - return err + log.Error().Err(fmt.Errorf("error when running cmd %s: %w", command.Cmd, err)).Send() + return outputArr, err } } - return nil + return outputArr, nil } -func cmdListWorker(id int, jobs <-chan *CmdList, config *BackyConfigFile, results chan<- string) { +func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, config *BackyConfigFile, results chan<- string) { + for list := range jobs { var currentCmd string fieldsMap := make(map[string]interface{}) @@ -173,7 +186,7 @@ func cmdListWorker(id int, jobs <-chan *CmdList, config *BackyConfigFile, result cmdLogger := config.Logger.With(). Str("backy-cmd", cmd). Logger() - runOutErr := cmdToRun.RunCmd(&cmdLogger, config.Hosts) + outputArr, runOutErr := cmdToRun.RunCmd(&cmdLogger, config.Hosts) count++ if runOutErr != nil { var errMsg bytes.Buffer @@ -183,8 +196,8 @@ func cmdListWorker(id int, jobs <-chan *CmdList, config *BackyConfigFile, result errStruct["Command"] = currentCmd errStruct["Err"] = runOutErr errStruct["CmdsRan"] = cmdsRan - t := template.Must(template.New("error.txt").ParseFS(templates, "templates/error.txt")) - tmpErr := t.Execute(&errMsg, errStruct) + errStruct["Output"] = outputArr + tmpErr := msgTemps.err.Execute(&errMsg, errStruct) if tmpErr != nil { config.Logger.Err(tmpErr).Send() } @@ -204,8 +217,7 @@ func cmdListWorker(id int, jobs <-chan *CmdList, config *BackyConfigFile, result successStruct := make(map[string]interface{}) successStruct["listName"] = list.Name successStruct["CmdsRan"] = cmdsRan - t := template.Must(template.New("success.txt").ParseFS(templates, "templates/success.txt")) - tmpErr := t.Execute(&successMsg, successStruct) + tmpErr := msgTemps.success.Execute(&successMsg, successStruct) if tmpErr != nil { config.Logger.Err(tmpErr).Send() break @@ -228,6 +240,10 @@ func cmdListWorker(id int, jobs <-chan *CmdList, config *BackyConfigFile, result // RunBackyConfig runs a command list from the BackyConfigFile. func (config *BackyConfigFile) RunBackyConfig(cron string) { + mTemps := &msgTemplates{ + err: template.Must(template.New("error.txt").ParseFS(templates, "templates/error.txt")), + success: template.Must(template.New("success.txt").ParseFS(templates, "templates/success.txt")), + } configListsLen := len(config.CmdConfigLists) listChan := make(chan *CmdList, configListsLen) results := make(chan string) @@ -235,8 +251,7 @@ func (config *BackyConfigFile) RunBackyConfig(cron string) { // This starts up 3 workers, initially blocked // because there are no jobs yet. for w := 1; w <= configListsLen; w++ { - go cmdListWorker(w, listChan, config, results) - + go cmdListWorker(mTemps, listChan, config, results) } // Here we send 5 `jobs` and then `close` that @@ -262,11 +277,16 @@ func (config *BackyConfigFile) RunBackyConfig(cron string) { } -func (config *BackyConfigFile) ExecuteCmds() { - for _, cmd := range config.Cmds { - runErr := cmd.RunCmd(&config.Logger, config.Hosts) +func (config *BackyConfigFile) ExecuteCmds(opts *BackyConfigOpts) { + for _, cmd := range opts.executeCmds { + cmdToRun := config.Cmds[cmd] + cmdLogger := config.Logger.With(). + Str("backy-cmd", cmd). + Logger() + _, runErr := cmdToRun.RunCmd(&cmdLogger, config.Hosts) if runErr != nil { config.Logger.Err(runErr).Send() } } + } diff --git a/pkg/backy/config.go b/pkg/backy/config.go index 07b5c93..dfca7a1 100644 --- a/pkg/backy/config.go +++ b/pkg/backy/config.go @@ -71,12 +71,11 @@ func ReadConfig(opts *BackyConfigOpts) *BackyConfigFile { os.Setenv("BACKY_LOGLEVEL", Sprintf("%v", globalLvl)) } - consoleLoggingEnabled := backyViper.GetBool(getLoggingKeyFromConfig("console")) + consoleLoggingDisabled := backyViper.GetBool(getLoggingKeyFromConfig("console-disabled")) + os.Setenv("BACKY_CONSOLE_LOGGING", "enabled") // Other qualifiers can go here as well - if consoleLoggingEnabled { - os.Setenv("BACKY_CONSOLE_LOGGING", "enabled") - } else { + if consoleLoggingDisabled { os.Setenv("BACKY_CONSOLE_LOGGING", "") } @@ -119,7 +118,10 @@ func ReadConfig(opts *BackyConfigOpts) *BackyConfigFile { if unmarshalErr != nil { panic(fmt.Errorf("error unmarshalling hosts struct: %w", unmarshalErr)) } - for _, host := range backyConfigFile.Hosts { + for hostConfigName, host := range backyConfigFile.Hosts { + if host.Host == "" { + host.Host = hostConfigName + } if host.ProxyJump != "" { proxyHosts := strings.Split(host.ProxyJump, ",") if len(proxyHosts) > 1 { @@ -153,6 +155,7 @@ func ReadConfig(opts *BackyConfigOpts) *BackyConfigFile { } } } + cmdListCfg := backyViper.Sub("cmd-configs") unmarshalErr = cmdListCfg.Unmarshal(&backyConfigFile.CmdConfigLists) if unmarshalErr != nil { diff --git a/pkg/backy/ssh.go b/pkg/backy/ssh.go index 0da0f1b..55772cb 100644 --- a/pkg/backy/ssh.go +++ b/pkg/backy/ssh.go @@ -20,7 +20,7 @@ import ( "golang.org/x/crypto/ssh/knownhosts" ) -var ErrPrivateKeyFileFailedToOpen = errors.New("Failed to open private key file. If encrypted, make sure the password is specified.") +var PrivateKeyExtraInfoErr = errors.New("Private key may be encrypted. \nIf encrypted, make sure the password is specified correctly in the correct section: \n privatekeypassword: password (not recommended) \n privatekeypassword: env:PR_KEY_PASS \n privatekeypassword: file:/path/to/password-file. \n ") var TS = strings.TrimSpace // ConnectToSSHHost connects to a host by looking up the config values in the directory ~/.ssh/config @@ -38,16 +38,6 @@ func (remoteConfig *Host) ConnectToSSHHost(log *zerolog.Logger, hosts map[string if TS(remoteConfig.ConfigFilePath) == "" { remoteConfig.useDefaultConfig = true } - - if remoteConfig.ProxyHost != nil { - for _, proxyHost := range remoteConfig.ProxyHost { - log.Info().Msgf("Proxy Host %s", proxyHost.Host) - err := proxyHost.GetProxyJumpConfig(hosts) - if err != nil { - return err - } - } - } khPath, khPathErr := GetKnownHosts(remoteConfig.KnownHostsFile) if khPathErr != nil { @@ -77,6 +67,20 @@ func (remoteConfig *Host) ConnectToSSHHost(log *zerolog.Logger, hosts map[string if decodeErr != nil { return decodeErr } + + err := remoteConfig.GetProxyJumpFromConfig(hosts) + if err != nil { + return err + } + if remoteConfig.ProxyHost != nil { + for _, proxyHost := range remoteConfig.ProxyHost { + log.Info().Msgf("Proxy Host %s", proxyHost.Host) + err := proxyHost.GetProxyJumpConfig(hosts) + if err != nil { + return err + } + } + } remoteConfig.ClientConfig.Timeout = time.Second * 30 remoteConfig.GetPrivateKeyFileFromConfig() remoteConfig.GetPort() @@ -86,7 +90,7 @@ func (remoteConfig *Host) ConnectToSSHHost(log *zerolog.Logger, hosts map[string if remoteConfig.HostName == "" { return errors.New("No hostname found or specified") } - err := remoteConfig.GetAuthMethods() + err = remoteConfig.GetAuthMethods() if err != nil { return err } @@ -146,13 +150,13 @@ func (remoteHost *Host) GetAuthMethods() error { if remoteHost.PrivateKeyPassword == "" { signer, err = ssh.ParsePrivateKey(privateKey) if err != nil { - return ErrPrivateKeyFileFailedToOpen + return errors.Errorf("Failed to open private key file %s: %v \n\n %v", remoteHost.PrivateKeyPath, err, PrivateKeyExtraInfoErr) } remoteHost.ClientConfig.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)} } else { signer, err = ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(remoteHost.PrivateKeyPassword)) if err != nil { - return err + return errors.Errorf("Failed to open private key file %s: %v \n\n %v", remoteHost.PrivateKeyPath, err, PrivateKeyExtraInfoErr) } remoteHost.ClientConfig.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)} } @@ -193,7 +197,7 @@ func (remoteHost *Host) GetPrivateKeyFileFromConfig() { // GetPort checks if the port from the config file is 0 // If it is the port is searched in the SSH config file(s) func (remoteHost *Host) GetPort() { - port := fmt.Sprintf("%v", remoteHost.Port) + port := fmt.Sprintf("%d", remoteHost.Port) // port specifed? if port == "0" { port, _ = remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "Port") @@ -204,16 +208,16 @@ func (remoteHost *Host) GetPort() { } } } - portNum, _ := strconv.ParseUint(port, 10, 32) + portNum, _ := strconv.ParseUint(port, 10, 16) remoteHost.Port = uint16(portNum) } func (remoteHost *Host) CombineHostNameWithPort() { - port := fmt.Sprintf(":%v", remoteHost.Port) - if strings.Contains(remoteHost.HostName, port) { + port := fmt.Sprintf(":%d", remoteHost.Port) + if strings.HasSuffix(remoteHost.HostName, port) { return } - remoteHost.HostName = fmt.Sprintf("%s:%v", remoteHost.HostName, remoteHost.Port) + remoteHost.HostName = fmt.Sprintf("%s:%d", remoteHost.HostName, remoteHost.Port) } func (remoteHost *Host) GetHostName() { @@ -270,7 +274,7 @@ func GetPrivateKeyPassword(key string) (string, error) { privKeyPassFilePath, _ = resolveDir(privKeyPassFilePath) keyFile, keyFileErr := os.Open(privKeyPassFilePath) if keyFileErr != nil { - return "", ErrPrivateKeyFileFailedToOpen + return "", errors.Errorf("Private key password file %s failed to open. \n Make sure it is accessible and correct.", privKeyPassFilePath) } passwordScanner := bufio.NewScanner(keyFile) for passwordScanner.Scan() { @@ -332,8 +336,10 @@ func (remoteConfig *Host) GetProxyJumpFromConfig(hosts map[string]*Host) error { if proxyHostFound { remoteConfig.ProxyHost = append(remoteConfig.ProxyHost, proxyHost) } else { - newProxy := &Host{Host: proxyJump} - remoteConfig.ProxyHost = append(remoteConfig.ProxyHost, newProxy) + if proxyJump != "" { + newProxy := &Host{Host: proxyJump} + remoteConfig.ProxyHost = append(remoteConfig.ProxyHost, newProxy) + } } } @@ -344,7 +350,6 @@ func (remoteConfig *Host) GetProxyJumpConfig(hosts map[string]*Host) error { remoteConfig.useDefaultConfig = true } - // log.Info().Msgf("Proxy Host %s", remoteConfig.ProxyHost[0].Host) khPath, khPathErr := GetKnownHosts(remoteConfig.KnownHostsFile) if khPathErr != nil { diff --git a/pkg/backy/templates/error.txt b/pkg/backy/templates/error.txt index b40f345..9982dee 100644 --- a/pkg/backy/templates/error.txt +++ b/pkg/backy/templates/error.txt @@ -1,8 +1,12 @@ Command list {{.listName }} failed on running {{.Command}}. -The error was {{ .Err }} +{{ if .Err }} The error was {{ .Err }}{{ end }} +{{ if .Output }} The output was {{- range .Output}} {{.}} {{end}} {{end}} + +{{ if .CmdsRan }} The following commands ran: {{- range .CmdsRan}} - {{. -}} -{{end}} \ No newline at end of file +{{end}} +{{ end }} \ No newline at end of file diff --git a/pkg/backy/types.go b/pkg/backy/types.go index 4c6abe6..89d627d 100644 --- a/pkg/backy/types.go +++ b/pkg/backy/types.go @@ -1,10 +1,8 @@ -// types.go -// Copyright (C) Andrew Woodlee 2023 -// License: Apache-2.0 package backy import ( "bytes" + "text/template" "github.com/kevinburke/ssh_config" "github.com/nikoksr/notify" @@ -15,158 +13,158 @@ import ( "golang.org/x/crypto/ssh" ) -type CmdConfigSchema struct { - ID primitive.ObjectID `bson:"_id,omitempty"` - CmdList []string `bson:"command-list,omitempty"` - Name string `bson:"name,omitempty"` -} -type CmdSchema struct { - ID primitive.ObjectID `bson:"_id,omitempty"` - Cmd string `bson:"cmd,omitempty"` - Args []string `bson:"args,omitempty"` - Host string `bson:"host,omitempty"` - Dir string `bson:"dir,omitempty"` -} - -type Schemas struct { - CmdConfigSchema - CmdSchema -} - -// Host defines a host to which to connect. -// If not provided, the values will be looked up in the default ssh config files -type Host struct { - ConfigFilePath string `yaml:"configfilepath,omitempty"` - Host string `yaml:"host,omitempty"` - HostName string `yaml:"hostname,omitempty"` - KnownHostsFile string `yaml:"knownhostsfile,omitempty"` - ClientConfig *ssh.ClientConfig - SSHConfigFile *sshConfigFile - SshClient *ssh.Client - Port uint16 `yaml:"port,omitempty"` - ProxyJump string `yaml:"proxyjump,omitempty"` - Password string `yaml:"password,omitempty"` - PrivateKeyPath string `yaml:"privatekeypath,omitempty"` - PrivateKeyPassword string `yaml:"privatekeypassword,omitempty"` - UseConfigFiles bool `yaml:"use_config_files,omitempty"` - useDefaultConfig bool - User string `yaml:"user,omitempty"` - // ProxyHost holds the configuration for a ProxyJump host - ProxyHost []*Host -} - -type sshConfigFile struct { - SshConfigFile *ssh_config.Config - DefaultUserSettings *ssh_config.UserSettings -} - -type Command struct { - // Remote bool `yaml:"remote,omitempty"` - - Output BackyCommandOutput `yaml:"-"` - - // command to run - Cmd string `yaml:"cmd"` - - // host on which to run cmd - Host *string `yaml:"host,omitempty"` - - /* - Shell specifies which shell to run the command in, if any. - Not applicable when host is defined. - */ - Shell string `yaml:"shell,omitempty"` - - RemoteHost *Host `yaml:"-"` - - // Args is an array that holds the arguments to cmd - Args []string `yaml:"Args,omitempty"` - - /* - Dir specifies a directory in which to run the command. - Ignored if Host is set. - */ - Dir *string `yaml:"dir,omitempty"` - - // Env points to a file containing env variables to be used with the command - Env string `yaml:"env,omitempty"` - - // Environment holds env variables to be used with the command - Environment []string `yaml:"environment,omitempty"` -} - -type BackyOptionFunc func(*BackyConfigOpts) - -type CmdList struct { - Name string `yaml:"name,omitempty"` - Cron string `yaml:"cron,omitempty"` - Order []string `yaml:"order,omitempty"` - Notifications []string `yaml:"notifications,omitempty"` - NotifyConfig *notify.Notify - // NotificationsConfig map[string]*NotificationsConfig - // NotifyConfig map[string]*notify.Notify -} - -type BackyConfigFile struct { - - // Cmds holds the commands for a list. - // Key is the name of the command, - Cmds map[string]*Command `yaml:"commands"` - - // CmdConfigLists holds the lists of commands to be run in order. - // Key is the command list name. - CmdConfigLists map[string]*CmdList `yaml:"cmd-configs"` - - // Hosts holds the Host config. - // key is the host. - Hosts map[string]*Host `yaml:"hosts"` - - // Notifications holds the config for different notifications. - Notifications map[string]*NotificationsConfig - - Logger zerolog.Logger -} - -type BackyConfigOpts struct { - // Global log level - BackyLogLvl *string - // Holds config file - ConfigFile *BackyConfigFile - // Holds config file - ConfigFilePath string - - Schemas - - DB *mongo.Database - // use command lists using cron - useCron bool - // Holds commands to execute for the exec command - executeCmds []string - // Holds commands to execute for the exec command - executeLists []string - - // Holds env vars from .env file - backyEnv map[string]string - - viper *viper.Viper -} - -type NotificationsConfig struct { - Config *viper.Viper - Enabled bool -} - -type CmdOutput struct { - Err error - Output bytes.Buffer -} - -type BackyCommandOutput interface { - Error() error - GetOutput() CmdOutput -} - -type environmentVars struct { - file string - env []string -} +type ( + CmdConfigSchema struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + CmdList []string `bson:"command-list,omitempty"` + Name string `bson:"name,omitempty"` + } + + CmdSchema struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + Cmd string `bson:"cmd,omitempty"` + Args []string `bson:"args,omitempty"` + Host string `bson:"host,omitempty"` + Dir string `bson:"dir,omitempty"` + } + + Schemas struct { + CmdConfigSchema + CmdSchema + } + + // Host defines a host to which to connect. + // If not provided, the values will be looked up in the default ssh config files + Host struct { + ConfigFilePath string `yaml:"configfilepath,omitempty"` + Host string `yaml:"host,omitempty"` + HostName string `yaml:"hostname,omitempty"` + KnownHostsFile string `yaml:"knownhostsfile,omitempty"` + ClientConfig *ssh.ClientConfig + SSHConfigFile *sshConfigFile + SshClient *ssh.Client + Port uint16 `yaml:"port,omitempty"` + ProxyJump string `yaml:"proxyjump,omitempty"` + Password string `yaml:"password,omitempty"` + PrivateKeyPath string `yaml:"privatekeypath,omitempty"` + PrivateKeyPassword string `yaml:"privatekeypassword,omitempty"` + useDefaultConfig bool + User string `yaml:"user,omitempty"` + // ProxyHost holds the configuration for a ProxyJump host + ProxyHost []*Host + // CertPath string `yaml:"cert_path,omitempty"` + } + + sshConfigFile struct { + SshConfigFile *ssh_config.Config + DefaultUserSettings *ssh_config.UserSettings + } + + Command struct { + + // command to run + Cmd string `yaml:"cmd"` + + // host on which to run cmd + Host *string `yaml:"host,omitempty"` + + /* + Shell specifies which shell to run the command in, if any. + Not applicable when host is defined. + */ + Shell string `yaml:"shell,omitempty"` + + RemoteHost *Host `yaml:"-"` + + // Args is an array that holds the arguments to cmd + Args []string `yaml:"args,omitempty"` + + /* + Dir specifies a directory in which to run the command. + Ignored if Host is set. + */ + Dir *string `yaml:"dir,omitempty"` + + // Env points to a file containing env variables to be used with the command + Env string `yaml:"env,omitempty"` + + // Environment holds env variables to be used with the command + Environment []string `yaml:"environment,omitempty"` + } + + BackyOptionFunc func(*BackyConfigOpts) + + CmdList struct { + Name string `yaml:"name,omitempty"` + Cron string `yaml:"cron,omitempty"` + Order []string `yaml:"order,omitempty"` + Notifications []string `yaml:"notifications,omitempty"` + NotifyConfig *notify.Notify + // NotificationsConfig map[string]*NotificationsConfig + // NotifyConfig map[string]*notify.Notify + } + + BackyConfigFile struct { + + // Cmds holds the commands for a list. + // Key is the name of the command, + Cmds map[string]*Command `yaml:"commands"` + + // CmdConfigLists holds the lists of commands to be run in order. + // Key is the command list name. + CmdConfigLists map[string]*CmdList `yaml:"cmd-configs"` + + // Hosts holds the Host config. + // key is the host. + Hosts map[string]*Host `yaml:"hosts"` + + // Notifications holds the config for different notifications. + Notifications map[string]*NotificationsConfig + + Logger zerolog.Logger + } + + BackyConfigOpts struct { + // Global log level + BackyLogLvl *string + // Holds config file + ConfigFile *BackyConfigFile + // Holds config file + ConfigFilePath string + + Schemas + + DB *mongo.Database + // use command lists using cron + useCron bool + // Holds commands to execute for the exec command + executeCmds []string + // Holds commands to execute for the exec command + executeLists []string + + // Holds env vars from .env file + backyEnv map[string]string + + viper *viper.Viper + } + + NotificationsConfig struct { + Config *viper.Viper + Enabled bool + } + + CmdOutput struct { + Err error + Output bytes.Buffer + } + + environmentVars struct { + file string + env []string + } + + msgTemplates struct { + success *template.Template + err *template.Template + } +) diff --git a/pkg/backy/utils.go b/pkg/backy/utils.go index b09a4c2..fe7c44c 100644 --- a/pkg/backy/utils.go +++ b/pkg/backy/utils.go @@ -44,13 +44,12 @@ func injectEnvIntoSSH(envVarsToInject environmentVars, process *ssh.Session, log } errEnvFile: - if len(envVarsToInject.env) > 0 { - for _, envVal := range envVarsToInject.env { - // don't append env Vars for Backy - if strings.Contains(envVal, "=") && !strings.HasPrefix(envVal, "BACKY_") { - envVarArr := strings.Split(envVal, "=") - process.Setenv(envVarArr[0], envVarArr[1]) - } + // fmt.Printf("%v", envVarsToInject.env) + for _, envVal := range envVarsToInject.env { + // don't append env Vars for Backy + if strings.Contains(envVal, "=") { + envVarArr := strings.Split(envVal, "=") + process.Setenv(envVarArr[0], envVarArr[1]) } } } @@ -75,14 +74,13 @@ func injectEnvIntoLocalCMD(envVarsToInject environmentVars, process *exec.Cmd, l } errEnvFile: - if len(envVarsToInject.env) > 0 { - for _, envVal := range envVarsToInject.env { - if strings.Contains(envVal, "=") { - process.Env = append(process.Env, envVal) - } + + for _, envVal := range envVarsToInject.env { + if strings.Contains(envVal, "=") { + process.Env = append(process.Env, envVal) } } - envVarsToInject.env = append(envVarsToInject.env, os.Environ()...) + process.Env = append(process.Env, os.Environ()...) } func (cmd *Command) checkCmdExists() bool { @@ -104,9 +102,8 @@ func CheckConfigValues(config *viper.Viper) { for _, key := range requiredKeys { isKeySet := config.IsSet(key) if !isKeySet { - logging.ExitWithMSG(Sprintf("Config key %s is not defined in %s", key, config.ConfigFileUsed()), 1, nil) + logging.ExitWithMSG(Sprintf("Config key %s is not defined in %s. Please make sure this value is set and has the appropriate keys set.", key, config.ConfigFileUsed()), 1, nil) } - } } @@ -117,8 +114,6 @@ func testFile(c string) error { if errors.Is(fileOpenErr, os.ErrNotExist) { return fileOpenErr } - - fmt.Printf("%s\t\t%v", c, fileOpenErr) } return nil diff --git a/release b/release old mode 100644 new mode 100755 index f310143..8428d38 --- a/release +++ b/release @@ -1,4 +1,5 @@ #!/bin/bash -git tag "$(svu next)" -git push --tags -goreleaser --rm-dist \ No newline at end of file +export GORELEASER_CURRENT_TAG="$(go run backy.go version)" +git tag "$(go run backy.go version)" +git push all --tags +goreleaser release -f .goreleaser/gitea.yml --clean --release-notes=".changes/$(go run backy.go version)" \ No newline at end of file