11 Commits

Author SHA1 Message Date
951bf97eb2 fix for CI scripts 2023-02-18 22:52:10 -06:00
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
02321870b5 fix for remote host ports 2023-02-12 08:48:46 -06:00
3e9138e05a fix for remote host ports 2023-02-12 08:29:55 -06:00
51f9e9a776 Merge branch 'develop' 2023-02-11 23:52:00 -06:00
37c20aaafa added new features
- expanding `environment` vars in cmd section
- support for connecting to one proxy/bastion host
- better notification text layout
- better error message on private key failing to open
2023-02-11 23:50:19 -06:00
9c202cf3e9 Merge branch 'develop' 2023-02-02 11:28:35 -06:00
c3fa74e442 fix for remote host ports 2023-02-02 11:24:01 -06:00
0f3cf0d9c4 changed .goreleaser.yaml 2023-02-02 00:15:53 -06:00
30 changed files with 850 additions and 431 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.
@ -8,7 +6,12 @@ builds:
- env: - env:
- CGO_ENABLED=0 - CGO_ENABLED=0
goos: goos:
- freebsd
- linux - linux
goarch:
- "386"
- amd64
- arm64
archives: archives:
- format: tar.gz - format: tar.gz
@ -29,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

@ -14,11 +14,17 @@ You can also download binaries [here](https://git.andrewnw.xyz/CyberShell/backy/
## Features ## Features
- Define lists of commands and run them - Allows easy configuration of executable commands
- Execute commands over SSH - Allows for commands to be run on many hosts over SSH
- More to come. - Commands can be grouped in list to run in specific order
- Notifications on completion and failure
- Run in cron mode
- For any command, especially backup commands
To run a config: To run a config:
@ -29,84 +35,11 @@ Or to use a specific file:
If you leave the config path blank, the following paths will be searched in order: If you leave the config path blank, the following paths will be searched in order:
- `./backy.yml`
- `./backy.yaml` - `./backy.yaml`
- `~/.config/backy.yml`
- `~/.config/backy.yaml` - `~/.config/backy.yaml`
Create a file at `~/.config/backy.yaml`: Create a file at `~/.config/backy.yml`.
```yaml See the config file in the examples directory to configure it.
commands:
stop-docker-container:
cmd: docker
Args:
- compose
- -f /some/path/to/docker-compose.yaml
- down
# if host is not defined, cmd will be run locally
host: some-host
backup-docker-container-script:
cmd: /path/to/script
# The host has to be defined in the config file
host: some-host
shell-cmd:
cmd: rsync
shell: bash
Args:
- -av some-host:/path/to/data ~/Docker/Backups/docker-data
hostname:
cmd: hostname
cmd-configs:
cmds-to-run: # this can be any name you want
# all commands have to be defined
order:
- stop-docker-container
- backup-docker-container-script
- shell-cmd
- hostname
notifications:
- matrix
name: backup-some-server
hostname:
name: hostname
order:
- hostname
notifications:
- prod-email
hosts:
some-host:
hostname: some-hostname
config: ~/.ssh/config
user: user
privatekeypath: /path/to/private/key
port: 22
password:
logging:
verbose: true
file: /path/to/logs/commands.log
console: false
cmd-std-out: false
notifications:
prod-email:
id: prod-email
type: mail
host: yourhost.tld:port
senderAddress: email@domain.tld
to:
- admin@domain.tld
username: smtp-username@domain.tld
password: your-password-here
matrix:
id: matrix
type: matrix
home-server: your-home-server.tld
room-id: room-id
access-token: your-access-token
user-id: your-user-id
```

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,
} }
) )
@ -25,7 +24,7 @@ var cmdLists []string
func init() { func init() {
backupCmd.Flags().StringSliceVarP(&cmdLists, "lists", "l", nil, "Accepts a comma-separated names of command lists to execute.") backupCmd.Flags().StringSliceVarP(&cmdLists, "lists", "l", nil, "Accepts comma-separated names of command lists to execute.")
} }

31
cmd/config.go Normal file
View File

@ -0,0 +1,31 @@
package cmd
// import (
// "git.andrewnw.xyz/CyberShell/backy/pkg/backy"
// "github.com/spf13/cobra"
// )
// var (
// configCmd = &cobra.Command{
// Use: "config list ...",
// Short: "Runs commands defined in config file.",
// Long: `Cron executes commands at the time defined in config file.`,
// Run: config,
// }
// cmds []string
// lists []string
// )
// func config(cmd *cobra.Command, args []string) {
// opts := backy.NewOpts(cfgFile, backy.UseCron())
// opts.InitConfig()
// }
// func init() {
// configCmd.PersistentFlags().StringArrayVarP(&cmds, "cmds", "c", nil, "Accepts comma-seperated list of commands to list")
// }

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("%s\n", versionStr)
os.Exit(0)
}

View File

@ -11,6 +11,9 @@ commands:
cmd: /path/to/script cmd: /path/to/script
# The host has to be defined in the config file # The host has to be defined in the config file
host: some-host host: some-host
environment:
- FOO=BAR
- APP=$VAR
shell-cmd: shell-cmd:
cmd: rsync cmd: rsync
shell: bash shell: bash
@ -30,6 +33,7 @@ cmd-configs:
notifications: notifications:
- matrix - matrix
name: backup-some-server name: backup-some-server
cron: "0 0 1 * * *"
hostname: hostname:
name: hostname name: hostname
order: order:
@ -38,15 +42,19 @@ cmd-configs:
- prod-email - prod-email
hosts: hosts:
# any ssh_config(5) keys/values not listed here will be looked up in the config file or the default config file
some-host: some-host:
hostname: some-hostname hostname: some-hostname
config: ~/.ssh/config config: ~/.ssh/config
user: user user: user
privatekeypath: /path/to/private/key privatekeypath: /path/to/private/key
port: 22 port: 22
password: # can also be env:VAR
password: file:/path/to/file
# only one is supported for now
proxyjump: some-proxy-host
# optional
logging: logging:
verbose: true verbose: true
file: /path/to/logs/commands.log file: /path/to/logs/commands.log
@ -58,7 +66,8 @@ notifications:
prod-email: prod-email:
id: prod-email id: prod-email
type: mail type: mail
host: yourhost.tld:port host: yourhost.tld
port: 587
senderAddress: email@domain.tld senderAddress: email@domain.tld
to: to:
- admin@domain.tld - admin@domain.tld

View File

@ -11,11 +11,17 @@ import (
"io" "io"
"os" "os"
"os/exec" "os/exec"
"text/template"
"embed"
"github.com/rs/zerolog" "github.com/rs/zerolog"
) )
var requiredKeys = []string{"commands", "cmd-configs", "logging"} //go:embed templates/*.txt
var templates embed.FS
var requiredKeys = []string{"commands", "cmd-configs"}
var Sprintf = fmt.Sprintf var Sprintf = fmt.Sprintf
@ -23,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) 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
@ -35,24 +42,23 @@ func (command *Command) RunCmd(log *zerolog.Logger) error {
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()
sshc, err := command.RemoteHost.ConnectToSSHHost(log) err := command.RemoteHost.ConnectToSSHHost(log, hosts)
if err != nil { if err != nil {
return err return nil, err
} }
defer sshc.Close() defer command.RemoteHost.SshClient.Close()
commandSession, err := sshc.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()
@ -75,12 +81,15 @@ func (command *Command) RunCmd(log *zerolog.Logger) error {
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()
@ -90,7 +99,7 @@ func (command *Command) RunCmd(log *zerolog.Logger) error {
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 {
@ -112,23 +121,29 @@ func (command *Command) RunCmd(log *zerolog.Logger) error {
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)
@ -141,24 +156,28 @@ func (command *Command) RunCmd(log *zerolog.Logger) error {
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{})
fieldsMap["list"] = list.Name fieldsMap["list"] = list.Name
cmdLog := config.Logger.Info() cmdLog := config.Logger.Info()
var count int var count int
var Msg string var cmdsRan []string
for _, cmd := range list.Order { for _, cmd := range list.Order {
currentCmd = config.Cmds[cmd].Cmd currentCmd = config.Cmds[cmd].Cmd
fieldsMap["cmd"] = config.Cmds[cmd].Cmd fieldsMap["cmd"] = config.Cmds[cmd].Cmd
@ -167,12 +186,22 @@ 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) outputArr, runOutErr := cmdToRun.RunCmd(&cmdLogger, config.Hosts)
count++ count++
if runOutErr != nil { if runOutErr != nil {
var errMsg bytes.Buffer
if list.NotifyConfig != nil { if list.NotifyConfig != nil {
notifySendErr := list.NotifyConfig.Send(context.Background(), fmt.Sprintf("List %s failed on command %s ", list.Name, cmd), errStruct := make(map[string]interface{})
fmt.Sprintf("List %s failed on command %s running command %s. \n Error: %v", list.Name, cmd, currentCmd, runOutErr)) errStruct["listName"] = list.Name
errStruct["Command"] = currentCmd
errStruct["Err"] = runOutErr
errStruct["CmdsRan"] = cmdsRan
errStruct["Output"] = outputArr
tmpErr := msgTemps.err.Execute(&errMsg, errStruct)
if tmpErr != nil {
config.Logger.Err(tmpErr).Send()
}
notifySendErr := list.NotifyConfig.Send(context.Background(), fmt.Sprintf("List %s failed on command %s ", list.Name, cmd), errMsg.String())
if notifySendErr != nil { if notifySendErr != nil {
config.Logger.Err(notifySendErr).Send() config.Logger.Err(notifySendErr).Send()
} }
@ -182,41 +211,56 @@ func cmdListWorker(id int, jobs <-chan *CmdList, config *BackyConfigFile, result
} else { } else {
if count == len(list.Order) { if count == len(list.Order) {
Msg += fmt.Sprintf("%s ", cmd) cmdsRan = append(cmdsRan, cmd)
var successMsg bytes.Buffer
if list.NotifyConfig != nil { if list.NotifyConfig != nil {
err := list.NotifyConfig.Send(context.Background(), fmt.Sprintf("List %s succeded", list.Name), successStruct := make(map[string]interface{})
fmt.Sprintf("Command list %s was completed successfully. The following commands ran:\n %s", list.Name, Msg)) successStruct["listName"] = list.Name
successStruct["CmdsRan"] = cmdsRan
tmpErr := msgTemps.success.Execute(&successMsg, successStruct)
if tmpErr != nil {
config.Logger.Err(tmpErr).Send()
break
}
err := list.NotifyConfig.Send(context.Background(), fmt.Sprintf("List %s succeded", list.Name), successMsg.String())
if err != nil { if err != nil {
config.Logger.Err(err).Send() config.Logger.Err(err).Send()
} }
} }
} else { } else {
Msg += fmt.Sprintf("%s, ", cmd) cmdsRan = append(cmdsRan, cmd)
} }
} }
} }
results <- "done" results <- "done"
} }
} }
// 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)
// 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 <= 3; 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
// channel to indicate that's all the work we have. // channel to indicate that's all the work we have.
// configChan <- config.Cmds // configChan <- config.Cmds
for _, cmdConfig := range config.CmdConfigLists { for listName, cmdConfig := range config.CmdConfigLists {
if cmdConfig.Name == "" {
cmdConfig.Name = listName
}
if cron != "" { if cron != "" {
if cron == cmdConfig.Cron { if cron == cmdConfig.Cron {
listChan <- cmdConfig listChan <- cmdConfig
@ -233,8 +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 {
cmd.RunCmd(&config.Logger) 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()
}
} }
} }

View File

@ -12,7 +12,6 @@ import (
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/spf13/viper" "github.com/spf13/viper"
"mvdan.cc/sh/v3/shell"
) )
// ReadConfig validates and reads the config file. // ReadConfig validates and reads the config file.
@ -28,13 +27,12 @@ func ReadConfig(opts *BackyConfigOpts) *BackyConfigFile {
backyConfigFile := NewConfig() backyConfigFile := NewConfig()
backyViper := opts.viper backyViper := opts.viper
// loadEnv(backyViper) opts.loadEnv()
envFileInConfigDir := fmt.Sprintf("%s/.env", path.Dir(backyViper.ConfigFileUsed())) envFileInConfigDir := fmt.Sprintf("%s/.env", path.Dir(backyViper.ConfigFileUsed()))
envFileErr := godotenv.Load() // load the .env file in config file directory
if envFileErr != nil { _ = godotenv.Load(envFileInConfigDir)
_ = godotenv.Load(envFileInConfigDir)
}
if backyViper.GetBool(getNestedConfig("logging", "cmd-std-out")) { if backyViper.GetBool(getNestedConfig("logging", "cmd-std-out")) {
os.Setenv("BACKY_STDOUT", "enabled") os.Setenv("BACKY_STDOUT", "enabled")
} }
@ -52,38 +50,42 @@ func ReadConfig(opts *BackyConfigOpts) *BackyConfigFile {
} }
} }
var backyLoggingOpts *viper.Viper var (
isBackyLoggingOptsSet := backyViper.IsSet("logging") // backyLoggingOpts *viper.Viper
if isBackyLoggingOptsSet { verbose bool
backyLoggingOpts = backyViper.Sub("logging") logFile string
} )
verbose := backyLoggingOpts.GetBool("verbose")
logFile := backyLoggingOpts.GetString("file") verbose = backyViper.GetBool(getLoggingKeyFromConfig("verbose"))
logFile = fmt.Sprintf("%s/backy.log", path.Dir(backyViper.ConfigFileUsed()))
if backyViper.IsSet(getLoggingKeyFromConfig("file")) {
logFile = backyViper.GetString(getLoggingKeyFromConfig("file"))
}
zerolog.SetGlobalLevel(zerolog.InfoLevel) zerolog.SetGlobalLevel(zerolog.InfoLevel)
if verbose { if verbose {
zerolog.SetGlobalLevel(zerolog.DebugLevel) zerolog.SetGlobalLevel(zerolog.DebugLevel)
globalLvl := zerolog.GlobalLevel() globalLvl := zerolog.GlobalLevel()
os.Setenv("BACKY_LOGLEVEL", Sprintf("%x", globalLvl)) os.Setenv("BACKY_LOGLEVEL", Sprintf("%v", globalLvl))
} }
consoleLoggingEnabled := backyLoggingOpts.GetBool("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", "")
} }
writers := logging.SetLoggingWriters(backyLoggingOpts, logFile) writers := logging.SetLoggingWriters(logFile)
log := zerolog.New(writers).With().Timestamp().Logger() log := zerolog.New(writers).With().Timestamp().Logger()
backyConfigFile.Logger = log backyConfigFile.Logger = log
log.Info().Str("config file", backyViper.ConfigFileUsed()).Send()
commandsMap := backyViper.GetStringMapString("commands") commandsMap := backyViper.GetStringMapString("commands")
commandsMapViper := backyViper.Sub("commands") commandsMapViper := backyViper.Sub("commands")
unmarshalErr := commandsMapViper.Unmarshal(&backyConfigFile.Cmds) unmarshalErr := commandsMapViper.Unmarshal(&backyConfigFile.Cmds)
@ -100,6 +102,8 @@ func ReadConfig(opts *BackyConfigOpts) *BackyConfigFile {
os.Exit(1) os.Exit(1)
} }
expandEnvVars(opts.backyEnv, cmdConf.Environment)
host := cmdConf.Host host := cmdConf.Host
if host != nil { if host != nil {
if backyViper.IsSet(getNestedConfig("hosts", *host)) { if backyViper.IsSet(getNestedConfig("hosts", *host)) {
@ -114,15 +118,44 @@ 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 _, v := range backyConfigFile.Hosts { for hostConfigName, host := range backyConfigFile.Hosts {
if host.Host == "" {
if v.JumpHost != "" { host.Host = hostConfigName
proxyHost, defined := backyConfigFile.Hosts[v.JumpHost] }
if defined { if host.ProxyJump != "" {
v.ProxyHost = proxyHost proxyHosts := strings.Split(host.ProxyJump, ",")
if len(proxyHosts) > 1 {
for hostNum, h := range proxyHosts {
if hostNum > 1 {
proxyHost, defined := backyConfigFile.Hosts[h]
if defined {
host.ProxyHost = append(host.ProxyHost, proxyHost)
} else {
newProxy := &Host{Host: h}
host.ProxyHost = append(host.ProxyHost, newProxy)
}
} else {
proxyHost, defined := backyConfigFile.Hosts[h]
if defined {
host.ProxyHost = append(host.ProxyHost, proxyHost)
} else {
newHost := &Host{Host: h}
host.ProxyHost = append(host.ProxyHost, newHost)
}
}
}
} else {
proxyHost, defined := backyConfigFile.Hosts[proxyHosts[0]]
if defined {
host.ProxyHost = append(host.ProxyHost, proxyHost)
} else {
newProxy := &Host{Host: proxyHosts[0]}
host.ProxyHost = append(host.ProxyHost, newProxy)
}
} }
} }
} }
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 {
@ -157,10 +190,7 @@ func ReadConfig(opts *BackyConfigOpts) *BackyConfigFile {
cmdNotFoundErrorLog.Errs("commands not found", cmdNotFoundSliceErr).Send() cmdNotFoundErrorLog.Errs("commands not found", cmdNotFoundSliceErr).Send()
} }
if opts.useCron && len(backyConfigFile.CmdConfigLists) > 0 { if opts.useCron && (len(backyConfigFile.CmdConfigLists) == 0) {
log.Info().Msg("Starting cron mode...")
} else if opts.useCron && (len(backyConfigFile.CmdConfigLists) == 0) {
logging.ExitWithMSG("No cron fields detected in any command lists", 1, nil) logging.ExitWithMSG("No cron fields detected in any command lists", 1, nil)
} }
@ -217,6 +247,14 @@ func getNestedConfig(nestedConfig, key string) string {
func getCmdFromConfig(key string) string { func getCmdFromConfig(key string) string {
return fmt.Sprintf("commands.%s", key) return fmt.Sprintf("commands.%s", key)
} }
func getLoggingKeyFromConfig(key string) string {
if key == "" {
return "logging"
}
return fmt.Sprintf("logging.%s", key)
}
func getCmdListFromConfig(list string) string { func getCmdListFromConfig(list string) string {
return fmt.Sprintf("cmd-configs.%s", list) return fmt.Sprintf("cmd-configs.%s", list)
} }
@ -228,8 +266,13 @@ func (opts *BackyConfigOpts) InitConfig() {
backyViper := viper.New() backyViper := viper.New()
if strings.TrimSpace(opts.ConfigFilePath) != "" { if strings.TrimSpace(opts.ConfigFilePath) != "" {
err := testFile(opts.ConfigFilePath)
if err != nil {
logging.ExitWithMSG(fmt.Sprintf("Could not open config file %s: %v", opts.ConfigFilePath, err), 1, nil)
}
backyViper.SetConfigFile(opts.ConfigFilePath) backyViper.SetConfigFile(opts.ConfigFilePath)
} else { } else {
backyViper.SetConfigName("backy.yml") // name of config file (with extension)
backyViper.SetConfigName("backy.yaml") // name of config file (with extension) backyViper.SetConfigName("backy.yaml") // name of config file (with extension)
backyViper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name backyViper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name
backyViper.AddConfigPath(".") // optionally look for config in the working directory backyViper.AddConfigPath(".") // optionally look for config in the working directory
@ -237,40 +280,8 @@ func (opts *BackyConfigOpts) InitConfig() {
} }
err := backyViper.ReadInConfig() // Find and read the config file err := backyViper.ReadInConfig() // Find and read the config file
if err != nil { // Handle errors reading the config file if err != nil { // Handle errors reading the config file
panic(fmt.Errorf("fatal error reading config file %s: %w", backyViper.ConfigFileUsed(), err)) msg := fmt.Sprintf("fatal error reading config file %s: %v", backyViper.ConfigFileUsed(), err)
logging.ExitWithMSG(msg, 1, nil)
} }
opts.viper = backyViper opts.viper = backyViper
} }
func loadEnv(backyViper *viper.Viper) {
envFileInConfigDir := fmt.Sprintf("%s/.env", path.Dir(backyViper.ConfigFileUsed()))
var backyEnv map[string]string
backyEnv, envFileErr := godotenv.Read()
// envFile, envFileErr := os.Open(".env")
if envFileErr != nil {
backyEnv, _ = godotenv.Read(envFileInConfigDir)
}
envFileErr = godotenv.Load()
if envFileErr != nil {
_ = godotenv.Load(envFileInConfigDir)
}
env := func(name string) string {
name = strings.ToUpper(name)
envVar, found := backyEnv[name]
if found {
return envVar
}
return ""
}
envVars := []string{"APP=${BACKY_APP}"}
for indx, v := range envVars {
if strings.Contains(v, "$") || (strings.Contains(v, "${") && strings.Contains(v, "}")) {
out, _ := shell.Expand(v, env)
envVars[indx] = out
// println(out)
}
}
}

View File

@ -14,15 +14,21 @@ import (
func (conf *BackyConfigFile) Cron() { func (conf *BackyConfigFile) Cron() {
s := gocron.NewScheduler(time.Local) s := gocron.NewScheduler(time.Local)
s.TagsUnique() s.TagsUnique()
for _, config := range conf.CmdConfigLists { for listName, config := range conf.CmdConfigLists {
if strings.TrimSpace(config.Cron) != "" { if config.Name == "" {
_, err := s.CronWithSeconds(config.Cron).Tag(config.Name).Do(func(cron string) { config.Name = listName
}
cron := strings.TrimSpace(config.Cron)
if cron != "" {
conf.Logger.Info().Str("Scheduling cron list", config.Name).Str("Time", cron).Send()
_, err := s.CronWithSeconds(cron).Tag(config.Name).Do(func(cron string) {
conf.RunBackyConfig(cron) conf.RunBackyConfig(cron)
}, config.Cron) }, cron)
if err != nil { if err != nil {
panic(err) panic(err)
} }
} }
} }
conf.Logger.Info().Msg("Starting cron mode...")
s.StartBlocking() s.StartBlocking()
} }

View File

@ -87,5 +87,6 @@ func setupMail(config mailConfig) *mail.Mail {
mailClient := mail.New(config.senderaddress, config.host+":"+config.port) mailClient := mail.New(config.senderaddress, config.host+":"+config.port)
mailClient.AuthenticateSMTP("", config.username, config.password, config.host) mailClient.AuthenticateSMTP("", config.username, config.password, config.host)
mailClient.AddReceivers(config.to...) mailClient.AddReceivers(config.to...)
mailClient.BodyFormat(mail.PlainText)
return mailClient return mailClient
} }

View File

@ -7,9 +7,9 @@ package backy
import ( import (
"bufio" "bufio"
"fmt" "fmt"
"log"
"os" "os"
"os/user" "os/user"
"strconv"
"strings" "strings"
"time" "time"
@ -20,83 +20,102 @@ import (
"golang.org/x/crypto/ssh/knownhosts" "golang.org/x/crypto/ssh/knownhosts"
) )
var ErrPrivateKeyFileFailedToOpen = errors.New("Private key file failed to open.") 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
// It uses any set values and looks up an unset values in the config files // It uses any set values and looks up an unset values in the config files
// It returns an ssh.Client used to run commands against. // It returns an ssh.Client used to run commands against.
func (remoteConfig *Host) ConnectToSSHHost(log *zerolog.Logger) (*ssh.Client, error) { // If configFile is empty, any required configuration is looked up in the default config files
// If any value is not found, defaults are used
func (remoteConfig *Host) ConnectToSSHHost(log *zerolog.Logger, hosts map[string]*Host) error {
var sshClient *ssh.Client // var sshClient *ssh.Client
var connectErr error var connectErr error
// TODO: add JumpHost config check // TODO: add JumpHost config check
// if !remoteConfig.UseConfigFiles {
// log.Info().Msg("Not using config files")
// }
if TS(remoteConfig.ConfigFilePath) == "" { if TS(remoteConfig.ConfigFilePath) == "" {
remoteConfig.useDefaultConfig = true remoteConfig.useDefaultConfig = true
} }
khPath, khPathErr := GetKnownHosts(remoteConfig.KnownHostsFile) khPath, khPathErr := GetKnownHosts(remoteConfig.KnownHostsFile)
if khPathErr != nil { if khPathErr != nil {
return nil, khPathErr return khPathErr
} }
if remoteConfig.ClientConfig == nil { if remoteConfig.ClientConfig == nil {
remoteConfig.ClientConfig = &ssh.ClientConfig{} remoteConfig.ClientConfig = &ssh.ClientConfig{}
} }
var sshConfigFile *os.File var configFile *os.File
var sshConfigFileOpenErr error var sshConfigFileOpenErr error
if !remoteConfig.useDefaultConfig { if !remoteConfig.useDefaultConfig {
configFile, sshConfigFileOpenErr = os.Open(remoteConfig.ConfigFilePath)
sshConfigFile, sshConfigFileOpenErr = os.Open(remoteConfig.ConfigFilePath)
if sshConfigFileOpenErr != nil { if sshConfigFileOpenErr != nil {
return nil, sshConfigFileOpenErr return sshConfigFileOpenErr
} }
} else { } else {
defaultConfig, _ := resolveDir("~/.ssh/config") defaultConfig, _ := resolveDir("~/.ssh/config")
sshConfigFile, sshConfigFileOpenErr = os.Open(defaultConfig) configFile, sshConfigFileOpenErr = os.Open(defaultConfig)
if sshConfigFileOpenErr != nil { if sshConfigFileOpenErr != nil {
return nil, sshConfigFileOpenErr return sshConfigFileOpenErr
} }
} }
remoteConfig.SSHConfigFile = &sshConfigFile{}
remoteConfig.SSHConfigFile.DefaultUserSettings = ssh_config.DefaultUserSettings remoteConfig.SSHConfigFile.DefaultUserSettings = ssh_config.DefaultUserSettings
var decodeErr error
cfg, decodeErr := ssh_config.Decode(sshConfigFile) remoteConfig.SSHConfigFile.SshConfigFile, decodeErr = ssh_config.Decode(configFile)
if decodeErr != nil { if decodeErr != nil {
return nil, decodeErr return decodeErr
} }
remoteConfig.SSHConfigFile.SshConfigFile = cfg
remoteConfig.GetPrivateKeyFromConfig()
remoteConfig.GetHostNameWithPort()
remoteConfig.GetSshUserFromConfig()
if remoteConfig.HostName == "" { err := remoteConfig.GetProxyJumpFromConfig(hosts)
return nil, errors.New("No hostname found or specified")
}
err := remoteConfig.GetAuthMethods()
if err != nil { if err != nil {
return nil, err 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()
remoteConfig.GetHostName()
remoteConfig.CombineHostNameWithPort()
remoteConfig.GetSshUserFromConfig()
if remoteConfig.HostName == "" {
return errors.New("No hostname found or specified")
}
err = remoteConfig.GetAuthMethods()
if err != nil {
return err
} }
// TODO: Add value/option to config for host key and add bool to check for host key
hostKeyCallback, err := knownhosts.New(khPath) hostKeyCallback, err := knownhosts.New(khPath)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "could not create hostkeycallback function") return errors.Wrap(err, "could not create hostkeycallback function")
} }
remoteConfig.ClientConfig.HostKeyCallback = hostKeyCallback remoteConfig.ClientConfig.HostKeyCallback = hostKeyCallback
log.Info().Str("user", remoteConfig.ClientConfig.User).Send() log.Info().Str("user", remoteConfig.ClientConfig.User).Send()
log.Info().Msgf("Connecting to host %s", remoteConfig.HostName) remoteConfig.SshClient, connectErr = remoteConfig.ConnectThroughBastion(log)
remoteConfig.ClientConfig.Timeout = time.Second * 30
sshClient, connectErr = ssh.Dial("tcp", remoteConfig.HostName, remoteConfig.ClientConfig)
if connectErr != nil { if connectErr != nil {
return nil, connectErr return connectErr
} }
return sshClient, nil if remoteConfig.SshClient != nil {
return nil
}
log.Info().Msgf("Connecting to host %s", remoteConfig.HostName)
remoteConfig.SshClient, connectErr = ssh.Dial("tcp", remoteConfig.HostName, remoteConfig.ClientConfig)
if connectErr != nil {
return connectErr
}
return nil
} }
func (remoteHost *Host) GetSshUserFromConfig() { func (remoteHost *Host) GetSshUserFromConfig() {
@ -131,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)}
} }
@ -155,9 +174,9 @@ func (remoteHost *Host) GetAuthMethods() error {
// GetPrivateKeyFromConfig checks to see if the privateKeyPath is empty. // GetPrivateKeyFromConfig checks to see if the privateKeyPath is empty.
// If not, it keeps the value. // If not, it keeps the value.
// If empty, the key is looked for in the specified config file. // If empty, the key is looked for in the specified config file.
// If that path is empty, the default config file is searched // If that path is empty, the default config file is searched.
// If not found in the default file, the privateKeyPath is set to ~/.ssh/id_rsa // If not found in the default file, the privateKeyPath is set to ~/.ssh/id_rsa
func (remoteHost *Host) GetPrivateKeyFromConfig() { func (remoteHost *Host) GetPrivateKeyFileFromConfig() {
var identityFile string var identityFile string
if remoteHost.PrivateKeyPath == "" { if remoteHost.PrivateKeyPath == "" {
identityFile, _ = remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "IdentityFile") identityFile, _ = remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "IdentityFile")
@ -175,40 +194,51 @@ func (remoteHost *Host) GetPrivateKeyFromConfig() {
remoteHost.PrivateKeyPath, _ = resolveDir(identityFile) remoteHost.PrivateKeyPath, _ = resolveDir(identityFile)
} }
// GetHostNameWithPort checks if the port from the config file is empty // GetPort checks if the port from the config file is 0
// If it is the port is searched in the SSH config file // If it is the port is searched in the SSH config file(s)
func (remoteHost *Host) GetHostNameWithPort() { func (remoteHost *Host) GetPort() {
var port string port := fmt.Sprintf("%d", remoteHost.Port)
// port specifed?
if remoteHost.Port == 0 { if port == "0" {
if remoteHost.HostName == "" { port, _ = remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "Port")
remoteHost.HostName, _ = remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "HostName") if port == "" {
if remoteHost.HostName == "" { port = remoteHost.SSHConfigFile.DefaultUserSettings.Get(remoteHost.Host, "Port")
remoteHost.HostName = remoteHost.SSHConfigFile.DefaultUserSettings.Get(remoteHost.Host, "HostName")
}
port, _ = remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "Port")
if port == "" { if port == "" {
port = remoteHost.SSHConfigFile.DefaultUserSettings.Get(remoteHost.Host, "Port") port = "22"
if port == "" {
port = "22"
}
} }
} }
remoteHost.HostName = remoteHost.HostName + ":" + port }
} else { portNum, _ := strconv.ParseUint(port, 10, 16)
remoteHost.Port = uint16(portNum)
}
func (remoteHost *Host) CombineHostNameWithPort() {
port := fmt.Sprintf(":%d", remoteHost.Port)
if strings.HasSuffix(remoteHost.HostName, port) {
return
}
remoteHost.HostName = fmt.Sprintf("%s:%d", remoteHost.HostName, remoteHost.Port)
}
func (remoteHost *Host) GetHostName() {
if remoteHost.HostName == "" {
remoteHost.HostName, _ = remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "HostName")
if remoteHost.HostName == "" { if remoteHost.HostName == "" {
remoteHost.HostName, _ = remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "HostName") remoteHost.HostName = remoteHost.SSHConfigFile.DefaultUserSettings.Get(remoteHost.Host, "HostName")
if remoteHost.HostName == "" {
remoteHost.HostName = remoteHost.SSHConfigFile.DefaultUserSettings.Get(remoteHost.Host, "HostName")
}
} }
remoteHost.HostName = remoteHost.HostName + ":" + fmt.Sprintf("%v", remoteHost.Port)
} }
} }
func (remoteHost *Host) ConnectThroughBastion() (*ssh.Client, error) { func (remoteHost *Host) ConnectThroughBastion(log *zerolog.Logger) (*ssh.Client, error) {
if remoteHost.ProxyHost == nil {
return nil, nil
}
log.Info().Msgf("Connecting to proxy host %s", remoteHost.ProxyHost[0].HostName)
// connect to the bastion host // connect to the bastion host
bClient, err := ssh.Dial("tcp", remoteHost.ProxyHost.HostName, remoteHost.ProxyHost.ClientConfig) bClient, err := ssh.Dial("tcp", remoteHost.ProxyHost[0].HostName, remoteHost.ProxyHost[0].ClientConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -218,10 +248,10 @@ func (remoteHost *Host) ConnectThroughBastion() (*ssh.Client, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
log.Info().Msgf("Connecting to host %s", remoteHost.HostName)
ncc, chans, reqs, err := ssh.NewClientConn(conn, remoteHost.HostName, remoteHost.ClientConfig) ncc, chans, reqs, err := ssh.NewClientConn(conn, remoteHost.HostName, remoteHost.ClientConfig)
if err != nil { if err != nil {
log.Fatal(err) return nil, err
} }
sClient := ssh.NewClient(ncc, chans, reqs) sClient := ssh.NewClient(ncc, chans, reqs)
@ -244,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() {
@ -262,14 +292,14 @@ func GetPrivateKeyPassword(key string) (string, error) {
return prKeyPassword, nil return prKeyPassword, nil
} }
func GetPassword(key string) (string, error) { func GetPassword(pass string) (string, error) {
key = strings.TrimSpace(key) pass = strings.TrimSpace(pass)
if key == "" { if pass == "" {
return "", nil return "", nil
} }
var password string var password string
if strings.HasPrefix(key, "file:") { if strings.HasPrefix(pass, "file:") {
passFilePath := strings.TrimPrefix(key, "file:") passFilePath := strings.TrimPrefix(pass, "file:")
passFilePath, _ = resolveDir(passFilePath) passFilePath, _ = resolveDir(passFilePath)
keyFile, keyFileErr := os.Open(passFilePath) keyFile, keyFileErr := os.Open(passFilePath)
if keyFileErr != nil { if keyFileErr != nil {
@ -279,14 +309,95 @@ func GetPassword(key string) (string, error) {
for passwordScanner.Scan() { for passwordScanner.Scan() {
password = passwordScanner.Text() password = passwordScanner.Text()
} }
} else if strings.HasPrefix(key, "env:") { } else if strings.HasPrefix(pass, "env:") {
passEnv := strings.TrimPrefix(key, "env:") passEnv := strings.TrimPrefix(pass, "env:")
passEnv = strings.TrimPrefix(passEnv, "${") passEnv = strings.TrimPrefix(passEnv, "${")
passEnv = strings.TrimSuffix(passEnv, "}") passEnv = strings.TrimSuffix(passEnv, "}")
passEnv = strings.TrimPrefix(passEnv, "$") passEnv = strings.TrimPrefix(passEnv, "$")
password = os.Getenv(passEnv) password = os.Getenv(passEnv)
} else { } else {
password = key password = pass
} }
return password, nil return password, nil
} }
func (remoteConfig *Host) GetProxyJumpFromConfig(hosts map[string]*Host) error {
proxyJump, _ := remoteConfig.SSHConfigFile.SshConfigFile.Get(remoteConfig.Host, "ProxyJump")
if proxyJump == "" {
proxyJump = remoteConfig.SSHConfigFile.DefaultUserSettings.Get(remoteConfig.Host, "ProxyJump")
}
if remoteConfig.ProxyJump == "" && proxyJump != "" {
remoteConfig.ProxyJump = proxyJump
}
proxyJumpHosts := strings.Split(remoteConfig.ProxyJump, ",")
if remoteConfig.ProxyHost == nil && len(proxyJumpHosts) == 1 {
remoteConfig.ProxyJump = proxyJump
proxyHost, proxyHostFound := hosts[proxyJump]
if proxyHostFound {
remoteConfig.ProxyHost = append(remoteConfig.ProxyHost, proxyHost)
} else {
if proxyJump != "" {
newProxy := &Host{Host: proxyJump}
remoteConfig.ProxyHost = append(remoteConfig.ProxyHost, newProxy)
}
}
}
return nil
}
func (remoteConfig *Host) GetProxyJumpConfig(hosts map[string]*Host) error {
if TS(remoteConfig.ConfigFilePath) == "" {
remoteConfig.useDefaultConfig = true
}
khPath, khPathErr := GetKnownHosts(remoteConfig.KnownHostsFile)
if khPathErr != nil {
return khPathErr
}
if remoteConfig.ClientConfig == nil {
remoteConfig.ClientConfig = &ssh.ClientConfig{}
}
var configFile *os.File
var sshConfigFileOpenErr error
if !remoteConfig.useDefaultConfig {
configFile, sshConfigFileOpenErr = os.Open(remoteConfig.ConfigFilePath)
if sshConfigFileOpenErr != nil {
return sshConfigFileOpenErr
}
} else {
defaultConfig, _ := resolveDir("~/.ssh/config")
configFile, sshConfigFileOpenErr = os.Open(defaultConfig)
if sshConfigFileOpenErr != nil {
return sshConfigFileOpenErr
}
}
remoteConfig.SSHConfigFile = &sshConfigFile{}
remoteConfig.SSHConfigFile.DefaultUserSettings = ssh_config.DefaultUserSettings
var decodeErr error
remoteConfig.SSHConfigFile.SshConfigFile, decodeErr = ssh_config.Decode(configFile)
if decodeErr != nil {
return decodeErr
}
remoteConfig.GetPrivateKeyFileFromConfig()
remoteConfig.GetPort()
remoteConfig.GetHostName()
remoteConfig.CombineHostNameWithPort()
remoteConfig.GetSshUserFromConfig()
if remoteConfig.HostName == "" {
return errors.New("No hostname found or specified")
}
err := remoteConfig.GetAuthMethods()
if err != nil {
return err
}
// TODO: Add value/option to config for host key and add bool to check for host key
hostKeyCallback, err := knownhosts.New(khPath)
if err != nil {
return errors.Wrap(err, "could not create hostkeycallback function")
}
remoteConfig.ClientConfig.HostKeyCallback = hostKeyCallback
return nil
}

View File

@ -0,0 +1,12 @@
Command list {{.listName }} failed on running {{.Command}}.
{{ 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}}
{{ end }}

View File

@ -0,0 +1,7 @@
Command list {{ .listName }} was completed successfully.
The following commands ran:
{{- range .CmdsRan}}
- {{. -}}
{{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,154 +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
Port uint16 `yaml:"port,omitempty"`
JumpHost string `yaml:"jumphost,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 JumpHost 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
viper *viper.Viper // Holds env vars from .env file
} backyEnv map[string]string
type NotificationsConfig struct { viper *viper.Viper
Config *viper.Viper }
Enabled bool
}
type CmdOutput struct { NotificationsConfig struct {
Err error Config *viper.Viper
Output bytes.Buffer Enabled bool
} }
type BackyCommandOutput interface { CmdOutput struct {
Error() error Err error
GetOutput() CmdOutput Output bytes.Buffer
} }
type environmentVars struct { environmentVars struct {
file string file string
env []string env []string
} }
msgTemplates struct {
success *template.Template
err *template.Template
}
)

View File

@ -9,6 +9,7 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"path"
"path/filepath" "path/filepath"
"strings" "strings"
@ -17,6 +18,7 @@ import (
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/spf13/viper" "github.com/spf13/viper"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"mvdan.cc/sh/v3/shell"
) )
func injectEnvIntoSSH(envVarsToInject environmentVars, process *ssh.Session, log *zerolog.Logger) { func injectEnvIntoSSH(envVarsToInject environmentVars, process *ssh.Session, log *zerolog.Logger) {
@ -42,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])
}
} }
} }
} }
@ -73,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 {
@ -102,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)
} }
} }
} }
@ -115,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
@ -207,3 +204,33 @@ func resolveDir(path string) (string, error) {
} }
return path, nil return path, nil
} }
func (opts *BackyConfigOpts) loadEnv() {
envFileInConfigDir := fmt.Sprintf("%s/.env", path.Dir(opts.viper.ConfigFileUsed()))
var backyEnv map[string]string
backyEnv, envFileErr := godotenv.Read(envFileInConfigDir)
if envFileErr != nil {
return
}
opts.backyEnv = backyEnv
}
func expandEnvVars(backyEnv map[string]string, envVars []string) {
env := func(name string) string {
name = strings.ToUpper(name)
envVar, found := backyEnv[name]
if found {
return envVar
}
return ""
}
for indx, v := range envVars {
if strings.Contains(v, "$") || (strings.Contains(v, "${") && strings.Contains(v, "}")) {
out, _ := shell.Expand(v, env)
envVars[indx] = out
}
}
}

View File

@ -7,7 +7,6 @@ import (
"time" "time"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/spf13/viper"
"gopkg.in/natefinch/lumberjack.v2" "gopkg.in/natefinch/lumberjack.v2"
) )
@ -25,7 +24,7 @@ func ExitWithMSG(msg string, code int, log *zerolog.Logger) {
os.Exit(code) os.Exit(code)
} }
func SetLoggingWriters(v *viper.Viper, logFile string) (writers zerolog.LevelWriter) { func SetLoggingWriters(logFile string) (writers zerolog.LevelWriter) {
console := zerolog.ConsoleWriter{} console := zerolog.ConsoleWriter{}
if IsConsoleLoggingEnabled() { if IsConsoleLoggingEnabled() {
@ -55,12 +54,7 @@ func SetLoggingWriters(v *viper.Viper, logFile string) (writers zerolog.LevelWri
MaxAge: 28, //days MaxAge: 28, //days
Compress: true, // disabled by default Compress: true, // disabled by default
} }
if strings.TrimSpace(logFile) != "" { fileLogger.Filename = logFile
fileLogger.Filename = logFile
} else {
fileLogger.Filename = "./backy.log"
}
// UNIX Time is faster and smaller than most timestamps // UNIX Time is faster and smaller than most timestamps
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
// zerolog.TimeFieldFormat = time.RFC1123 // zerolog.TimeFieldFormat = time.RFC1123
@ -75,3 +69,7 @@ func SetLoggingWriters(v *viper.Viper, logFile string) (writers zerolog.LevelWri
func IsConsoleLoggingEnabled() bool { func IsConsoleLoggingEnabled() bool {
return os.Getenv("BACKY_CONSOLE_LOGGING") == "enabled" return os.Getenv("BACKY_CONSOLE_LOGGING") == "enabled"
} }
// func IsTerminal() bool {
// return os.Getenv("BACKY_TERM") == "enabled"
// }

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"