Version 0.2.4

* Notifications now display errors and the output of the failed command.
* CI configs for GitHub and Woodpecker
* Added `version` subcommand
* Console logging can be disabled by setting `console-disabled` in the
`logging` object
* 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.
master
Andrew 1 year ago
parent 02321870b5
commit ee83586072

@ -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.

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

@ -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_

@ -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 }}

@ -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: before:
hooks: hooks:
# You may remove this if you don't use go modules. # You may remove this if you don't use go modules.
@ -19,7 +17,7 @@ archives:
- format: tar.gz - format: tar.gz
# this name template makes the OS and Arch compatible with the results of uname. # this name template makes the OS and Arch compatible with the results of uname.
name_template: >- name_template: >-
{{ .ProjectName }} {{ .ProjectName }}_{{ .Version }}_
{{- title .Os }}_ {{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64 {{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386 {{- else if eq .Arch "386" }}i386
@ -34,11 +32,7 @@ checksum:
snapshot: snapshot:
name_template: "{{ incpatch .Version }}-next" name_template: "{{ incpatch .Version }}-next"
changelog: changelog:
sort: asc skip: false
filters:
exclude:
- '^docs:'
- '^test:'
gitea_urls: gitea_urls:
api: https://git.andrewnw.xyz/api/v1 api: https://git.andrewnw.xyz/api/v1

@ -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:'

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

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

@ -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.

@ -1,5 +1,8 @@
build: build:
go build go build
gorealeaser-build: install:
goreleaser release --snapshot --rm-dist go install .
goreleaser-snapshot:
goreleaser -f .goreleaser/gitea.yml release --snapshot --clean

@ -12,11 +12,10 @@ import (
var ( var (
backupCmd = &cobra.Command{ backupCmd = &cobra.Command{
Use: "backup [--lists==list1,list2]", Use: "backup [--lists=list1,list2]",
Short: "Runs commands defined in config file.", Short: "Runs commands defined in config file.",
Long: `Backup executes commands defined in config file. 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.",
Use the --lists flag to execute the specified commands.`, Run: Backup,
Run: Backup,
} }
) )

@ -8,9 +8,9 @@ import (
var ( var (
cronCmd = &cobra.Command{ cronCmd = &cobra.Command{
Use: "cron command ...", Use: "cron [flags]",
Short: "Runs commands defined in config file.", Short: "Runs command lists defined in config file.",
Long: `Cron executes commands at the time defined in config file.`, Long: `Cron starts a scheduler that executes command lists at the time defined in config file.`,
Run: cron, Run: cron,
} }
) )

@ -29,6 +29,5 @@ func execute(cmd *cobra.Command, args []string) {
opts := backy.NewOpts(cfgFile, backy.AddCommands(args)) opts := backy.NewOpts(cfgFile, backy.AddCommands(args))
opts.InitConfig() opts.InitConfig()
// opts.InitMongo() // opts.InitMongo()
backy.ReadConfig(opts).ExecuteCmds() backy.ReadConfig(opts).ExecuteCmds(opts)
} }

@ -36,5 +36,5 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "f", "", "config file to read from") rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "f", "", "config file to read from")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Sets verbose level") rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Sets verbose level")
rootCmd.AddCommand(backupCmd, execCmd, cronCmd) rootCmd.AddCommand(backupCmd, execCmd, cronCmd, versionCmd)
} }

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

@ -29,9 +29,10 @@ var Sprintf = fmt.Sprintf
// The environment of local commands will be the machine's environment plus any extra // The environment of local commands will be the machine's environment plus any extra
// variables specified in the Env file or Environment. // variables specified in the Env file or Environment.
// Dir can also be specified for local commands. // Dir can also be specified for local commands.
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 ( var (
outputArr []string
ArgsStr string ArgsStr string
cmdOutBuf bytes.Buffer cmdOutBuf bytes.Buffer
cmdOutWriters io.Writer cmdOutWriters io.Writer
@ -41,24 +42,23 @@ func (command *Command) RunCmd(log *zerolog.Logger, hosts map[string]*Host) erro
env: command.Environment, env: command.Environment,
} }
) )
envVars.env = append(envVars.env, os.Environ()...)
for _, v := range command.Args { for _, v := range command.Args {
ArgsStr += fmt.Sprintf(" %s", v) ArgsStr += fmt.Sprintf(" %s", v)
} }
if command.Host != nil { 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) err := command.RemoteHost.ConnectToSSHHost(log, hosts)
if err != nil { if err != nil {
return err return nil, err
} }
defer command.RemoteHost.SshClient.Close() defer command.RemoteHost.SshClient.Close()
commandSession, err := command.RemoteHost.SshClient.NewSession() commandSession, err := command.RemoteHost.SshClient.NewSession()
if err != nil { if err != nil {
log.Err(fmt.Errorf("new ssh session: %w", err)).Send() log.Err(fmt.Errorf("new ssh session: %w", err)).Send()
return err return nil, err
} }
defer commandSession.Close() 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 := make(map[string]interface{})
outMap["cmd"] = cmd outMap["cmd"] = cmd
outMap["output"] = outScanner.Text() outMap["output"] = outScanner.Text()
if str, ok := outMap["output"].(string); ok {
outputArr = append(outputArr, str)
}
log.Info().Fields(outMap).Send() log.Info().Fields(outMap).Send()
} }
if err != nil { if err != nil {
log.Error().Err(fmt.Errorf("error when running cmd: %s: %w", command.Cmd, err)).Send() log.Error().Err(fmt.Errorf("error when running cmd: %s: %w", command.Cmd, err)).Send()
return err return outputArr, err
} }
} else { } else {
cmdExists := command.checkCmdExists() cmdExists := command.checkCmdExists()
@ -96,7 +99,7 @@ func (command *Command) RunCmd(log *zerolog.Logger, hosts map[string]*Host) erro
var err error var err error
if command.Shell != "" { 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) ArgsStr = fmt.Sprintf("%s %s", command.Cmd, ArgsStr)
localCMD := exec.Command(command.Shell, "-c", ArgsStr) localCMD := exec.Command(command.Shell, "-c", ArgsStr)
if command.Dir != nil { 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 := make(map[string]interface{})
outMap["cmd"] = command.Cmd outMap["cmd"] = command.Cmd
outMap["output"] = outScanner.Text() outMap["output"] = outScanner.Text()
if str, ok := outMap["output"].(string); ok {
outputArr = append(outputArr, str)
}
log.Info().Fields(outMap).Send() log.Info().Fields(outMap).Send()
} }
if err != nil { if err != nil {
log.Error().Err(fmt.Errorf("error when running cmd: %s: %w", command.Cmd, err)).Send() log.Error().Err(fmt.Errorf("error when running cmd %s: %w", command.Cmd, err)).Send()
return err 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...) localCMD := exec.Command(command.Cmd, command.Args...)
if command.Dir != nil { if command.Dir != nil {
localCMD.Dir = *command.Dir localCMD.Dir = *command.Dir
} }
// fmt.Printf("%v\n", envVars.env)
injectEnvIntoLocalCMD(envVars, localCMD, log) injectEnvIntoLocalCMD(envVars, localCMD, log)
cmdOutWriters = io.MultiWriter(&cmdOutBuf) cmdOutWriters = io.MultiWriter(&cmdOutBuf)
// fmt.Printf("%v\n", localCMD.Environ())
if IsCmdStdOutEnabled() { if IsCmdStdOutEnabled() {
cmdOutWriters = io.MultiWriter(os.Stdout, &cmdOutBuf) 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 := make(map[string]interface{})
outMap["cmd"] = command.Cmd outMap["cmd"] = command.Cmd
outMap["output"] = outScanner.Text() outMap["output"] = outScanner.Text()
if str, ok := outMap["output"].(string); ok {
outputArr = append(outputArr, str)
}
log.Info().Fields(outMap).Send() log.Info().Fields(outMap).Send()
} }
if err != nil { if err != nil {
log.Error().Err(fmt.Errorf("error when running cmd: %s: %w", command.Cmd, err)).Send() log.Error().Err(fmt.Errorf("error when running cmd %s: %w", command.Cmd, err)).Send()
return err 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 { for list := range jobs {
var currentCmd string var currentCmd string
fieldsMap := make(map[string]interface{}) fieldsMap := make(map[string]interface{})
@ -173,7 +186,7 @@ func cmdListWorker(id int, jobs <-chan *CmdList, config *BackyConfigFile, result
cmdLogger := config.Logger.With(). cmdLogger := config.Logger.With().
Str("backy-cmd", cmd). Str("backy-cmd", cmd).
Logger() Logger()
runOutErr := cmdToRun.RunCmd(&cmdLogger, config.Hosts) outputArr, runOutErr := cmdToRun.RunCmd(&cmdLogger, config.Hosts)
count++ count++
if runOutErr != nil { if runOutErr != nil {
var errMsg bytes.Buffer var errMsg bytes.Buffer
@ -183,8 +196,8 @@ func cmdListWorker(id int, jobs <-chan *CmdList, config *BackyConfigFile, result
errStruct["Command"] = currentCmd errStruct["Command"] = currentCmd
errStruct["Err"] = runOutErr errStruct["Err"] = runOutErr
errStruct["CmdsRan"] = cmdsRan errStruct["CmdsRan"] = cmdsRan
t := template.Must(template.New("error.txt").ParseFS(templates, "templates/error.txt")) errStruct["Output"] = outputArr
tmpErr := t.Execute(&errMsg, errStruct) tmpErr := msgTemps.err.Execute(&errMsg, errStruct)
if tmpErr != nil { if tmpErr != nil {
config.Logger.Err(tmpErr).Send() 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 := make(map[string]interface{})
successStruct["listName"] = list.Name successStruct["listName"] = list.Name
successStruct["CmdsRan"] = cmdsRan successStruct["CmdsRan"] = cmdsRan
t := template.Must(template.New("success.txt").ParseFS(templates, "templates/success.txt")) tmpErr := msgTemps.success.Execute(&successMsg, successStruct)
tmpErr := t.Execute(&successMsg, successStruct)
if tmpErr != nil { if tmpErr != nil {
config.Logger.Err(tmpErr).Send() config.Logger.Err(tmpErr).Send()
break break
@ -228,6 +240,10 @@ func cmdListWorker(id int, jobs <-chan *CmdList, config *BackyConfigFile, result
// RunBackyConfig runs a command list from the BackyConfigFile. // RunBackyConfig runs a command list from the BackyConfigFile.
func (config *BackyConfigFile) RunBackyConfig(cron string) { 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) configListsLen := len(config.CmdConfigLists)
listChan := make(chan *CmdList, configListsLen) listChan := make(chan *CmdList, configListsLen)
results := make(chan string) results := make(chan string)
@ -235,8 +251,7 @@ func (config *BackyConfigFile) RunBackyConfig(cron string) {
// This starts up 3 workers, initially blocked // This starts up 3 workers, initially blocked
// because there are no jobs yet. // because there are no jobs yet.
for w := 1; w <= configListsLen; w++ { 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 // Here we send 5 `jobs` and then `close` that
@ -262,11 +277,16 @@ func (config *BackyConfigFile) RunBackyConfig(cron string) {
} }
func (config *BackyConfigFile) ExecuteCmds() { func (config *BackyConfigFile) ExecuteCmds(opts *BackyConfigOpts) {
for _, cmd := range config.Cmds { for _, cmd := range opts.executeCmds {
runErr := cmd.RunCmd(&config.Logger, config.Hosts) cmdToRun := config.Cmds[cmd]
cmdLogger := config.Logger.With().
Str("backy-cmd", cmd).
Logger()
_, runErr := cmdToRun.RunCmd(&cmdLogger, config.Hosts)
if runErr != nil { if runErr != nil {
config.Logger.Err(runErr).Send() config.Logger.Err(runErr).Send()
} }
} }
} }

@ -71,12 +71,11 @@ func ReadConfig(opts *BackyConfigOpts) *BackyConfigFile {
os.Setenv("BACKY_LOGLEVEL", Sprintf("%v", globalLvl)) 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 // Other qualifiers can go here as well
if consoleLoggingEnabled { if consoleLoggingDisabled {
os.Setenv("BACKY_CONSOLE_LOGGING", "enabled")
} else {
os.Setenv("BACKY_CONSOLE_LOGGING", "") os.Setenv("BACKY_CONSOLE_LOGGING", "")
} }
@ -119,7 +118,10 @@ func ReadConfig(opts *BackyConfigOpts) *BackyConfigFile {
if unmarshalErr != nil { if unmarshalErr != nil {
panic(fmt.Errorf("error unmarshalling hosts struct: %w", unmarshalErr)) 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 != "" { if host.ProxyJump != "" {
proxyHosts := strings.Split(host.ProxyJump, ",") proxyHosts := strings.Split(host.ProxyJump, ",")
if len(proxyHosts) > 1 { if len(proxyHosts) > 1 {
@ -153,6 +155,7 @@ func ReadConfig(opts *BackyConfigOpts) *BackyConfigFile {
} }
} }
} }
cmdListCfg := backyViper.Sub("cmd-configs") cmdListCfg := backyViper.Sub("cmd-configs")
unmarshalErr = cmdListCfg.Unmarshal(&backyConfigFile.CmdConfigLists) unmarshalErr = cmdListCfg.Unmarshal(&backyConfigFile.CmdConfigLists)
if unmarshalErr != nil { if unmarshalErr != nil {

@ -20,7 +20,7 @@ import (
"golang.org/x/crypto/ssh/knownhosts" "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 var TS = strings.TrimSpace
// ConnectToSSHHost connects to a host by looking up the config values in the directory ~/.ssh/config // 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) == "" { if TS(remoteConfig.ConfigFilePath) == "" {
remoteConfig.useDefaultConfig = true 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) khPath, khPathErr := GetKnownHosts(remoteConfig.KnownHostsFile)
if khPathErr != nil { if khPathErr != nil {
@ -77,6 +67,20 @@ func (remoteConfig *Host) ConnectToSSHHost(log *zerolog.Logger, hosts map[string
if decodeErr != nil { if decodeErr != nil {
return decodeErr 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.ClientConfig.Timeout = time.Second * 30
remoteConfig.GetPrivateKeyFileFromConfig() remoteConfig.GetPrivateKeyFileFromConfig()
remoteConfig.GetPort() remoteConfig.GetPort()
@ -86,7 +90,7 @@ func (remoteConfig *Host) ConnectToSSHHost(log *zerolog.Logger, hosts map[string
if remoteConfig.HostName == "" { if remoteConfig.HostName == "" {
return errors.New("No hostname found or specified") return errors.New("No hostname found or specified")
} }
err := remoteConfig.GetAuthMethods() err = remoteConfig.GetAuthMethods()
if err != nil { if err != nil {
return err return err
} }
@ -146,13 +150,13 @@ func (remoteHost *Host) GetAuthMethods() error {
if remoteHost.PrivateKeyPassword == "" { if remoteHost.PrivateKeyPassword == "" {
signer, err = ssh.ParsePrivateKey(privateKey) signer, err = ssh.ParsePrivateKey(privateKey)
if err != nil { 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)} remoteHost.ClientConfig.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)}
} else { } else {
signer, err = ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(remoteHost.PrivateKeyPassword)) signer, err = ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(remoteHost.PrivateKeyPassword))
if err != nil { 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)} 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 // GetPort checks if the port from the config file is 0
// If it is the port is searched in the SSH config file(s) // If it is the port is searched in the SSH config file(s)
func (remoteHost *Host) GetPort() { func (remoteHost *Host) GetPort() {
port := fmt.Sprintf("%v", remoteHost.Port) port := fmt.Sprintf("%d", remoteHost.Port)
// port specifed? // port specifed?
if port == "0" { if port == "0" {
port, _ = remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "Port") 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) remoteHost.Port = uint16(portNum)
} }
func (remoteHost *Host) CombineHostNameWithPort() { func (remoteHost *Host) CombineHostNameWithPort() {
port := fmt.Sprintf(":%v", remoteHost.Port) port := fmt.Sprintf(":%d", remoteHost.Port)
if strings.Contains(remoteHost.HostName, port) { if strings.HasSuffix(remoteHost.HostName, port) {
return 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() { func (remoteHost *Host) GetHostName() {
@ -270,7 +274,7 @@ func GetPrivateKeyPassword(key string) (string, error) {
privKeyPassFilePath, _ = resolveDir(privKeyPassFilePath) privKeyPassFilePath, _ = resolveDir(privKeyPassFilePath)
keyFile, keyFileErr := os.Open(privKeyPassFilePath) keyFile, keyFileErr := os.Open(privKeyPassFilePath)
if keyFileErr != nil { 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) passwordScanner := bufio.NewScanner(keyFile)
for passwordScanner.Scan() { for passwordScanner.Scan() {
@ -332,8 +336,10 @@ func (remoteConfig *Host) GetProxyJumpFromConfig(hosts map[string]*Host) error {
if proxyHostFound { if proxyHostFound {
remoteConfig.ProxyHost = append(remoteConfig.ProxyHost, proxyHost) remoteConfig.ProxyHost = append(remoteConfig.ProxyHost, proxyHost)
} else { } else {
newProxy := &Host{Host: proxyJump} if proxyJump != "" {
remoteConfig.ProxyHost = append(remoteConfig.ProxyHost, newProxy) 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 remoteConfig.useDefaultConfig = true
} }
// log.Info().Msgf("Proxy Host %s", remoteConfig.ProxyHost[0].Host)
khPath, khPathErr := GetKnownHosts(remoteConfig.KnownHostsFile) khPath, khPathErr := GetKnownHosts(remoteConfig.KnownHostsFile)
if khPathErr != nil { if khPathErr != nil {

@ -1,8 +1,12 @@
Command list {{.listName }} failed on running {{.Command}}. 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: The following commands ran:
{{- range .CmdsRan}} {{- range .CmdsRan}}
- {{. -}} - {{. -}}
{{end}} {{end}}
{{ end }}

@ -1,10 +1,8 @@
// types.go
// Copyright (C) Andrew Woodlee 2023
// License: Apache-2.0
package backy package backy
import ( import (
"bytes" "bytes"
"text/template"
"github.com/kevinburke/ssh_config" "github.com/kevinburke/ssh_config"
"github.com/nikoksr/notify" "github.com/nikoksr/notify"
@ -15,158 +13,158 @@ import (
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
type CmdConfigSchema struct { type (
ID primitive.ObjectID `bson:"_id,omitempty"` CmdConfigSchema struct {
CmdList []string `bson:"command-list,omitempty"` ID primitive.ObjectID `bson:"_id,omitempty"`
Name string `bson:"name,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"` CmdSchema struct {
Args []string `bson:"args,omitempty"` ID primitive.ObjectID `bson:"_id,omitempty"`
Host string `bson:"host,omitempty"` Cmd string `bson:"cmd,omitempty"`
Dir string `bson:"dir,omitempty"` Args []string `bson:"args,omitempty"`
} Host string `bson:"host,omitempty"`
Dir string `bson:"dir,omitempty"`
type Schemas struct { }
CmdConfigSchema
CmdSchema 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 { // Host defines a host to which to connect.
ConfigFilePath string `yaml:"configfilepath,omitempty"` // If not provided, the values will be looked up in the default ssh config files
Host string `yaml:"host,omitempty"` Host struct {
HostName string `yaml:"hostname,omitempty"` ConfigFilePath string `yaml:"configfilepath,omitempty"`
KnownHostsFile string `yaml:"knownhostsfile,omitempty"` Host string `yaml:"host,omitempty"`
ClientConfig *ssh.ClientConfig HostName string `yaml:"hostname,omitempty"`
SSHConfigFile *sshConfigFile KnownHostsFile string `yaml:"knownhostsfile,omitempty"`
SshClient *ssh.Client ClientConfig *ssh.ClientConfig
Port uint16 `yaml:"port,omitempty"` SSHConfigFile *sshConfigFile
ProxyJump string `yaml:"proxyjump,omitempty"` SshClient *ssh.Client
Password string `yaml:"password,omitempty"` Port uint16 `yaml:"port,omitempty"`
PrivateKeyPath string `yaml:"privatekeypath,omitempty"` ProxyJump string `yaml:"proxyjump,omitempty"`
PrivateKeyPassword string `yaml:"privatekeypassword,omitempty"` Password string `yaml:"password,omitempty"`
UseConfigFiles bool `yaml:"use_config_files,omitempty"` PrivateKeyPath string `yaml:"privatekeypath,omitempty"`
useDefaultConfig bool PrivateKeyPassword string `yaml:"privatekeypassword,omitempty"`
User string `yaml:"user,omitempty"` useDefaultConfig bool
// ProxyHost holds the configuration for a ProxyJump host User string `yaml:"user,omitempty"`
ProxyHost []*Host // ProxyHost holds the configuration for a ProxyJump host
} ProxyHost []*Host
// CertPath string `yaml:"cert_path,omitempty"`
type sshConfigFile struct { }
SshConfigFile *ssh_config.Config
DefaultUserSettings *ssh_config.UserSettings sshConfigFile struct {
} SshConfigFile *ssh_config.Config
DefaultUserSettings *ssh_config.UserSettings
type Command struct { }
// Remote bool `yaml:"remote,omitempty"`
Command struct {
Output BackyCommandOutput `yaml:"-"`
// command to run
// command to run Cmd string `yaml:"cmd"`
Cmd string `yaml:"cmd"`
// host on which to run cmd
// host on which to run cmd Host *string `yaml:"host,omitempty"`
Host *string `yaml:"host,omitempty"`
/*
/* Shell specifies which shell to run the command in, if any.
Shell specifies which shell to run the command in, if any. Not applicable when host is defined.
Not applicable when host is defined. */
*/ Shell string `yaml:"shell,omitempty"`
Shell string `yaml:"shell,omitempty"`
RemoteHost *Host `yaml:"-"`
RemoteHost *Host `yaml:"-"`
// Args is an array that holds the arguments to cmd
// Args is an array that holds the arguments to cmd Args []string `yaml:"args,omitempty"`
Args []string `yaml:"Args,omitempty"`
/*
/* Dir specifies a directory in which to run the command.
Dir specifies a directory in which to run the command. Ignored if Host is set.
Ignored if Host is set. */
*/ Dir *string `yaml:"dir,omitempty"`
Dir *string `yaml:"dir,omitempty"`
// Env points to a file containing env variables to be used with the command
// Env points to a file containing env variables to be used with the command Env string `yaml:"env,omitempty"`
Env string `yaml:"env,omitempty"`
// Environment holds env variables to be used with the command
// Environment holds env variables to be used with the command Environment []string `yaml:"environment,omitempty"`
Environment []string `yaml:"environment,omitempty"` }
}
BackyOptionFunc func(*BackyConfigOpts)
type BackyOptionFunc func(*BackyConfigOpts)
CmdList struct {
type CmdList struct { Name string `yaml:"name,omitempty"`
Name string `yaml:"name,omitempty"` Cron string `yaml:"cron,omitempty"`
Cron string `yaml:"cron,omitempty"` Order []string `yaml:"order,omitempty"`
Order []string `yaml:"order,omitempty"` Notifications []string `yaml:"notifications,omitempty"`
Notifications []string `yaml:"notifications,omitempty"` NotifyConfig *notify.Notify
NotifyConfig *notify.Notify // NotificationsConfig map[string]*NotificationsConfig
// NotificationsConfig map[string]*NotificationsConfig // NotifyConfig map[string]*notify.Notify
// NotifyConfig map[string]*notify.Notify }
}
BackyConfigFile struct {
type BackyConfigFile struct {
// Cmds holds the commands for a list.
// Cmds holds the commands for a list. // Key is the name of the command,
// Key is the name of the command, Cmds map[string]*Command `yaml:"commands"`
Cmds map[string]*Command `yaml:"commands"`
// CmdConfigLists holds the lists of commands to be run in order.
// CmdConfigLists holds the lists of commands to be run in order. // Key is the command list name.
// Key is the command list name. CmdConfigLists map[string]*CmdList `yaml:"cmd-configs"`
CmdConfigLists map[string]*CmdList `yaml:"cmd-configs"`
// Hosts holds the Host config.
// Hosts holds the Host config. // key is the host.
// key is the host. Hosts map[string]*Host `yaml:"hosts"`
Hosts map[string]*Host `yaml:"hosts"`
// Notifications holds the config for different notifications.
// Notifications holds the config for different notifications. Notifications map[string]*NotificationsConfig
Notifications map[string]*NotificationsConfig
Logger zerolog.Logger
Logger zerolog.Logger }
}
BackyConfigOpts struct {
type BackyConfigOpts struct { // Global log level
// Global log level BackyLogLvl *string
BackyLogLvl *string // Holds config file
// Holds config file ConfigFile *BackyConfigFile
ConfigFile *BackyConfigFile // Holds config file
// Holds config file ConfigFilePath string
ConfigFilePath string
Schemas
Schemas
DB *mongo.Database
DB *mongo.Database // use command lists using cron
// use command lists using cron useCron bool
useCron bool // Holds commands to execute for the exec command
// Holds commands to execute for the exec command executeCmds []string
executeCmds []string // Holds commands to execute for the exec command
// Holds commands to execute for the exec command executeLists []string
executeLists []string
// Holds env vars from .env file
// Holds env vars from .env file backyEnv map[string]string
backyEnv map[string]string
viper *viper.Viper
viper *viper.Viper }
}
NotificationsConfig struct {
type NotificationsConfig struct { Config *viper.Viper
Config *viper.Viper Enabled bool
Enabled bool }
}
CmdOutput struct {
type CmdOutput struct { Err error
Err error Output bytes.Buffer
Output bytes.Buffer }
}
environmentVars struct {
type BackyCommandOutput interface { file string
Error() error env []string
GetOutput() CmdOutput }
}
msgTemplates struct {
type environmentVars struct { success *template.Template
file string err *template.Template
env []string }
} )

@ -44,13 +44,12 @@ func injectEnvIntoSSH(envVarsToInject environmentVars, process *ssh.Session, log
} }
errEnvFile: errEnvFile:
if len(envVarsToInject.env) > 0 { // fmt.Printf("%v", envVarsToInject.env)
for _, envVal := range envVarsToInject.env { for _, envVal := range envVarsToInject.env {
// don't append env Vars for Backy // don't append env Vars for Backy
if strings.Contains(envVal, "=") && !strings.HasPrefix(envVal, "BACKY_") { if strings.Contains(envVal, "=") {
envVarArr := strings.Split(envVal, "=") envVarArr := strings.Split(envVal, "=")
process.Setenv(envVarArr[0], envVarArr[1]) process.Setenv(envVarArr[0], envVarArr[1])
}
} }
} }
} }
@ -75,14 +74,13 @@ func injectEnvIntoLocalCMD(envVarsToInject environmentVars, process *exec.Cmd, l
} }
errEnvFile: errEnvFile:
if len(envVarsToInject.env) > 0 {
for _, envVal := range envVarsToInject.env { for _, envVal := range envVarsToInject.env {
if strings.Contains(envVal, "=") { if strings.Contains(envVal, "=") {
process.Env = append(process.Env, 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 { func (cmd *Command) checkCmdExists() bool {
@ -104,9 +102,8 @@ func CheckConfigValues(config *viper.Viper) {
for _, key := range requiredKeys { for _, key := range requiredKeys {
isKeySet := config.IsSet(key) isKeySet := config.IsSet(key)
if !isKeySet { 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) { if errors.Is(fileOpenErr, os.ErrNotExist) {
return fileOpenErr return fileOpenErr
} }
fmt.Printf("%s\t\t%v", c, fileOpenErr)
} }
return nil return nil

@ -1,4 +1,5 @@
#!/bin/bash #!/bin/bash
git tag "$(svu next)" export GORELEASER_CURRENT_TAG="$(go run backy.go version)"
git push --tags git tag "$(go run backy.go version)"
goreleaser --rm-dist git push all --tags
goreleaser release -f .goreleaser/gitea.yml --clean --release-notes=".changes/$(go run backy.go version)"
Loading…
Cancel
Save