Compare commits

...

3 Commits

Author SHA1 Message Date
a2a89011fe fix for release 2023-02-18 22:49:05 -06:00
d893a2684e fix for CI scripts 2023-02-18 22:48:15 -06:00
ee83586072 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.
2023-02-18 22:42:15 -06:00
23 changed files with 462 additions and 223 deletions

9
.changes/0.2.4.md Normal file
View File

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

6
.changes/header.tpl.md Normal file
View File

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

View File

26
.changie.yaml Normal file
View File

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

37
.github/workflows/release.yml vendored Normal file
View File

@ -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).md" -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 }}

View File

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

41
.goreleaser/github.yml Normal file
View File

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

46
.goreleaser/vern.yml Normal file
View File

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

10
.woodpecker/vern.yml Normal file
View File

@ -0,0 +1,10 @@
pipeline:
release:
image: goreleaser/goreleaser
commands:
- goreleaser release -f .goreleaser/vern.yml --release-notes=".changes/$(go run backy.go version).md"
secrets: [ gitea_token ]
when:
event: tag
branches: master

17
CHANGELOG.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

25
cmd/version.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"`
Args []string `bson:"args,omitempty"`
Host string `bson:"host,omitempty"`
Dir string `bson:"dir,omitempty"`
}
type Schemas struct { CmdSchema struct {
CmdConfigSchema ID primitive.ObjectID `bson:"_id,omitempty"`
CmdSchema Cmd string `bson:"cmd,omitempty"`
} Args []string `bson:"args,omitempty"`
Host string `bson:"host,omitempty"`
Dir string `bson:"dir,omitempty"`
}
// Host defines a host to which to connect. Schemas struct {
// If not provided, the values will be looked up in the default ssh config files CmdConfigSchema
type Host struct { CmdSchema
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 { // Host defines a host to which to connect.
SshConfigFile *ssh_config.Config // If not provided, the values will be looked up in the default ssh config files
DefaultUserSettings *ssh_config.UserSettings 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"`
}
type Command struct { sshConfigFile struct {
// Remote bool `yaml:"remote,omitempty"` SshConfigFile *ssh_config.Config
DefaultUserSettings *ssh_config.UserSettings
}
Output BackyCommandOutput `yaml:"-"` Command struct {
// 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"`
} }
type BackyOptionFunc func(*BackyConfigOpts) BackyOptionFunc func(*BackyConfigOpts)
type CmdList struct { 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
} }
type BackyConfigFile struct { 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
} }
type BackyConfigOpts struct { 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
} }
type NotificationsConfig struct { NotificationsConfig struct {
Config *viper.Viper Config *viper.Viper
Enabled bool Enabled bool
} }
type CmdOutput struct { CmdOutput struct {
Err error Err error
Output bytes.Buffer Output bytes.Buffer
} }
type BackyCommandOutput interface { environmentVars struct {
Error() error file string
GetOutput() CmdOutput env []string
} }
type environmentVars struct { msgTemplates struct {
file string success *template.Template
env []string err *template.Template
} }
)

View File

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

8
release Normal file → Executable file
View File

@ -1,4 +1,6 @@
#!/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
git push all --tags
goreleaser release -f .goreleaser/gitea.yml --clean --release-notes=".changes/$(go run backy.go version).md"