Compare commits

...

34 Commits

Author SHA1 Message Date
01efeab13f update CI config
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed
ci/woodpecker/push/publish-docs Pipeline was successful
2024-11-20 00:21:50 -06:00
9a663f4260 update docs
All checks were successful
ci/woodpecker/push/publish-docs Pipeline was successful
2024-11-20 00:19:40 -06:00
7c635c36e0 update docs 2024-11-20 00:18:15 -06:00
fff33849da update docs and CI config
All checks were successful
ci/woodpecker/push/publish-docs Pipeline was successful
2024-11-20 00:13:20 -06:00
3391ffa4e6 Revert "update docs and CI config"
This reverts commit b7d1be495e.
2024-11-20 00:10:29 -06:00
b7d1be495e update docs and CI config 2024-11-20 00:08:16 -06:00
2daf2f130d update docs
All checks were successful
ci/woodpecker/push/publish-docs Pipeline was successful
2024-11-20 00:00:14 -06:00
d120c2ca8f update changelog and add link in docs
All checks were successful
ci/woodpecker/push/publish-docs Pipeline was successful
2024-11-19 23:56:09 -06:00
02fd04930d update CI config
All checks were successful
ci/woodpecker/push/publish-docs Pipeline was successful
2024-11-19 23:52:14 -06:00
10b0abe707 update CI config 2024-11-19 23:49:00 -06:00
291a954e9c update CI config
Some checks failed
ci/woodpecker/tag/go-lint Pipeline failed
ci/woodpecker/push/go-lint Pipeline failed
ci/woodpecker/tag/gitea Pipeline was successful
ci/woodpecker/release/go-lint Pipeline failed
2024-11-19 23:30:33 -06:00
e43eecf383 update CI config
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed
ci/woodpecker/tag/gitea Pipeline failed
ci/woodpecker/tag/go-lint Pipeline failed
2024-11-19 23:22:00 -06:00
ea676fe0da add version to CI configs
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed
ci/woodpecker/tag/gitea Pipeline failed
ci/woodpecker/tag/go-lint Pipeline failed
2024-11-19 23:08:34 -06:00
e73bd9ff3b update docs
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed
ci/woodpecker/tag/gitea Pipeline failed
ci/woodpecker/tag/go-lint Pipeline failed
2024-11-19 22:49:11 -06:00
fd9426181f update changelog 2024-11-19 20:22:20 -06:00
c25edc5d78 add changelog 2024-11-19 20:19:48 -06:00
aebfbda64e add changelog 2024-11-19 20:19:38 -06:00
5fe0629b0f update version and docs 2024-11-19 20:17:03 -06:00
7d6acd77b5 update version 2024-11-19 14:25:00 -06:00
9d646297c7 fix: check for nil Command.Hooks in ExecuteHooks [#12] 2024-11-15 10:46:27 -06:00
bf8d261cf3 added timeout to golangci-lint command 2024-11-14 21:18:14 -06:00
686cd0019a Added Changie files 2024-11-14 21:13:40 -06:00
b7b002bd72 Added: Hooks for Commands.[name]: error, success, and final. Closes [#12]
Added Command.generateLogger() method.

Fixed: make command logger be used for errors, not just when running the command.
2024-11-14 21:10:49 -06:00
b8a63f39f5 add working command hooks
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed
2024-11-11 22:44:28 -06:00
feacb83274 new commands.[name].type: script; update templates
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed
2024-10-15 08:49:23 -05:00
2aeb435167 new commands.[name].type: script; update templates 2024-10-15 08:49:13 -05:00
51f5084dd0 more work on env variable parsing 2024-09-13 20:47:59 -05:00
cf04e4456a run go mod tidy 2024-09-03 16:25:12 -05:00
25dc6225b3 [CI SKIP] add change files 2024-08-28 21:52:23 -05:00
a300f696d3 add list config file and relevant config in main config file. other minor changes
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed
2024-08-28 15:06:25 -05:00
8161aaa0a9 fix CI config
Some checks failed
ci/woodpecker/push/gitea Pipeline was successful
ci/woodpecker/push/go-lint Pipeline failed
2023-09-09 10:03:17 -05:00
a6bfabe22f v0.4.0
Some checks failed
ci/woodpecker/push/gitea Pipeline was successful
ci/woodpecker/push/go-lint Pipeline failed
2023-09-09 10:00:55 -05:00
a5466fc121 v0.4.0
Some checks failed
ci/woodpecker/push/gitea Pipeline was successful
ci/woodpecker/push/go-lint Pipeline failed
2023-09-09 10:00:00 -05:00
fbf2d9cbbc v0.4.0
Some checks failed
ci/woodpecker/push/gitea Pipeline was successful
ci/woodpecker/push/go-lint Pipeline failed
2023-09-09 01:11:05 -05:00
33 changed files with 707 additions and 369 deletions

9
.changes/v0.5.0.md Normal file
View File

@ -0,0 +1,9 @@
## v0.5.0 - 2024-11-19
### Added
* Lists can now go in a file. See docs for more information.
* commands.[name].type: script now opens `scriptEnvFile`.
* Hooks for Commands.[name]. Error, success, and final. [#12]
### Changed
* GetKnownHosts is now a method of Host
### Fixed
* make command logger be used for errors, not just when running the command

View File

@ -21,7 +21,7 @@ jobs:
- run: git fetch --force --tags - run: git fetch --force --tags
- uses: actions/setup-go@v4 - uses: actions/setup-go@v4
with: with:
go-version: '1.26' go-version: '1.20'
cache: true cache: true
# More assembly might be required: Docker logins, GPG, etc. It all depends # More assembly might be required: Docker logins, GPG, etc. It all depends
# on your needs. # on your needs.

View File

@ -1,3 +1,4 @@
version: 2
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.
@ -31,9 +32,9 @@ archives:
checksum: checksum:
name_template: 'checksums.txt' name_template: 'checksums.txt'
snapshot: snapshot:
name_template: "{{ incpatch .Version }}-next" version_template: "{{ incpatch .Version }}-next"
changelog: changelog:
skip: false disable: false
gitea_urls: gitea_urls:
api: https://git.andrewnw.xyz/api/v1 api: https://git.andrewnw.xyz/api/v1

View File

@ -1,5 +1,6 @@
# This is an example .goreleaser.yml file with some sensible defaults. # This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com # Make sure to check the documentation at https://goreleaser.com
version: 2
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.
@ -32,7 +33,7 @@ archives:
checksum: checksum:
name_template: 'checksums.txt' name_template: 'checksums.txt'
snapshot: snapshot:
name_template: "{{ incpatch .Version }}-next" version_template: "{{ incpatch .Version }}-next"
changelog: changelog:
sort: asc sort: asc
filters: filters:

View File

@ -1,5 +1,12 @@
{ {
"cSpell.words": [ "cSpell.words": [
"Cmds" "Cmds",
"knadh",
"koanf",
"mattn",
"maunium",
"mautrix",
"nikoksr",
"Strs"
] ]
} }

View File

@ -7,4 +7,6 @@ steps:
when: when:
event: tag event: tag
branches: master when:
- event: tag
branch: master

View File

@ -7,4 +7,8 @@ steps:
release: release:
image: golangci/golangci-lint:v1.53.3 image: golangci/golangci-lint:v1.53.3
commands: commands:
- golangci-lint run -v - golangci-lint run -v --timeout 5m
when:
- event: push
branch: develop

View File

@ -5,12 +5,15 @@ steps:
- git submodule foreach 'git fetch origin; git checkout $(git describe --tags `git rev-list --tags --max-count=1`);' - git submodule foreach 'git fetch origin; git checkout $(git describe --tags `git rev-list --tags --max-count=1`);'
- cd docs - cd docs
- hugo - hugo
when:
- event: push
branch: master
path: "docs/*"
deploy: deploy:
image: codingkoopa/git-rsync-openssh image: codingkoopa/git-rsync-openssh
commands: commands:
- cd docs - cd docs
- echo "151.101.210.132 deb.debian.org" >> /etc/hosts
- echo "nameserver 1.1.1.1" > /etc/resolv.conf - echo "nameserver 1.1.1.1" > /etc/resolv.conf
- mkdir ~/.ssh && chmod -R 700 ~/.ssh - mkdir ~/.ssh && chmod -R 700 ~/.ssh
# - apt update -y && apt install openssh-client rsync -y # - apt update -y && apt install openssh-client rsync -y
@ -24,7 +27,7 @@ steps:
- rsync -atv --delete --progress vangen/ backy@backy.cybershell.xyz:vangen-go - rsync -atv --delete --progress vangen/ backy@backy.cybershell.xyz:vangen-go
secrets: [ ssh_host_key, ssh_deploy_key, ssh_passphrase ] secrets: [ ssh_host_key, ssh_deploy_key, ssh_passphrase ]
when:
branches: master - event: push
when: branch: master
path: "docs/*" path: "docs/*"

View File

@ -6,6 +6,30 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie). and is generated by [Changie](https://github.com/miniscruff/changie).
## v0.5.0 - 2024-11-19
### Added
* Lists can now go in a file. See docs for more information.
* commands.[name].type: script now opens `scriptEnvFile`.
* Hooks for Commands.[name]. Error, success, and final. [#12]
### Changed
* GetKnownHosts is now a method of Host
### Fixed
* make command logger be used for errors, not just when running the command
## v0.4.0 - 2023-09-08
### Added
* Added `scriptEnvFile` to command object that allows one to specify an environment file (or any file really) when a `scriptFile` is run. Inspired by the practice of keeping environment variables and scripts or commands seperate.
* Basis for listing commands
### Changed
* BREAKING: Notifications object now takes the form of service.id, where service can be "mail" or "matrix" and id is a unique id for the service.
* BREAKING: Since the change to the notifications object, cmd-lists' inner map key 'notifications' must be of the form service.id. id must be defined for that service. See notifications docs for aviliable services.
* Config parser is now the simpler Koanf - Keys are now case-sensitive
* Log size limited to 50 Mb
## v0.3.1 - 2023-07-20 ## v0.3.1 - 2023-07-20
### Changed ### Changed
* If an SSH session failed to be created, the command would fail. This would be caused when restarting the SSH host. The SSH connection is attempted to be created again. If successful, the command is executed normally. * If an SSH session failed to be created, the command would fail. This would be caused when restarting the SSH host. The SSH connection is attempted to be created again. If successful, the command is executed normally.

View File

@ -20,7 +20,7 @@ package cmd
// func config(cmd *cobra.Command, args []string) { // func config(cmd *cobra.Command, args []string) {
// opts := backy.NewOpts(cfgFile, backy.UseCron()) // opts := backy.NewOpts(cfgFile, backy.cronEnabled())
// opts.InitConfig() // opts.InitConfig()
// } // }

View File

@ -17,7 +17,7 @@ var (
func cron(cmd *cobra.Command, args []string) { func cron(cmd *cobra.Command, args []string) {
opts := backy.NewOpts(cfgFile, backy.UseCron()) opts := backy.NewOpts(cfgFile, backy.CronEnabled())
opts.InitConfig() opts.InitConfig()
backy.ReadConfig(opts) backy.ReadConfig(opts)
opts.Cron() opts.Cron()

View File

@ -20,20 +20,30 @@ var (
) )
var listsToList []string var listsToList []string
var cmdsToList []string
func init() { func init() {
listCmd.Flags().StringSliceVarP(&listsToList, "lists", "l", nil, "Accepts comma-separated names of command lists to list.") listCmd.Flags().StringSliceVarP(&listsToList, "lists", "l", nil, "Accepts comma-separated names of command lists to list.")
listCmd.Flags().StringSliceVarP(&cmdsToList, "cmds", "c", nil, "Accepts comma-separated names of commands to list.")
} }
func List(cmd *cobra.Command, args []string) { func List(cmd *cobra.Command, args []string) {
opts := backy.NewOpts(cfgFile, backy.SetListsToSearch(cmdLists)) // settup based on whats passed in:
// - cmds
// - lists
// - if none, list all commands
if cmdLists != nil {
}
opts := backy.NewOpts(cfgFile)
opts.InitConfig() opts.InitConfig()
opts = backy.ReadConfig(opts) opts = backy.ReadConfig(opts)
opts.ListConfiguration() opts.ListCommand("rm-sn-db")
} }

View File

@ -7,7 +7,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
const versionStr = "0.4.0" const versionStr = "0.5.0"
var ( var (
versionCmd = &cobra.Command{ versionCmd = &cobra.Command{

2
docs/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
public/
resources/_gen

View File

@ -7,6 +7,8 @@ Backy is a tool for automating data backup and remote command execution. It can
Why the name Backy? Because I wanted an app for backups. Why the name Backy? Because I wanted an app for backups.
View the [changelog here](https://git.andrewnw.xyz/CyberShell/backy/src/branch/master/CHANGELOG.md).
{{% notice tip %}} {{% notice tip %}}
Feel free to open a [PR](https://git.andrewnw.xyz/CyberShell/backy/pulls), raise an [issue](https://git.andrewnw.xyz/CyberShell/backy/issues "Open a Gitea Issue")(s), or request new feature(s). Feel free to open a [PR](https://git.andrewnw.xyz/CyberShell/backy/pulls), raise an [issue](https://git.andrewnw.xyz/CyberShell/backy/issues "Open a Gitea Issue")(s), or request new feature(s).
{{% /notice %}} {{% /notice %}}

View File

@ -110,6 +110,7 @@ Usage:
backy list [--list=list1,list2,... | -l list1, list2,...] [ -cmd cmd1 cmd2 cmd3...] [flags] backy list [--list=list1,list2,... | -l list1, list2,...] [ -cmd cmd1 cmd2 cmd3...] [flags]
Flags: Flags:
-c, --cmds strings Accepts comma-separated names of commands to list.
-h, --help help for list -h, --help help for list
-l, --lists strings Accepts comma-separated names of command lists to list. -l, --lists strings Accepts comma-separated names of command lists to list.

View File

@ -12,10 +12,10 @@ 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` 1. `./backy.yml`
- `./backy.yaml` 2. `./backy.yaml`
- `~/.config/backy.yml` 3. `~/.config/backy.yml`
- `~/.config/backy.yaml` 4. `~/.config/backy.yaml`
Create a file at `~/.config/backy.yml`. Create a file at `~/.config/backy.yml`.

View File

@ -9,6 +9,11 @@ Command lists are for executing commands in sequence and getting notifications f
The top-level object key can be anything you want but not the same as another. The top-level object key can be anything you want but not the same as another.
Lists can go in a separate file. Command lists should be in a separate file if:
1. key 'cmd-lists.file' is found
2. hosts.yml or hosts.yaml is found in the same directory as the backy config file
```yaml ```yaml
test2: test2:
name: test2 name: test2
@ -51,7 +56,7 @@ An array of notification IDs to use on success and failure. Must match any of th
### Name ### Name
Name is optional for logging. If name is not defined, name will be the object's map key. Name is optional. If name is not defined, name will be the object's map key.
### Cron mode ### Cron mode

View File

@ -20,6 +20,13 @@ commands:
# if host is not defined, command will be run locally # if host is not defined, command will be run locally
# The host has to be defined in either the config file or the SSH Config files # The host has to be defined in either the config file or the SSH Config files
host: some-host host: some-host
hooks
error:
- some-other-command-when-failing
success:
- success-command
final:
- final-command
backup-docker-container-script: backup-docker-container-script:
cmd: /path/to/local/script cmd: /path/to/local/script
# script file is input as stdin to SSH # script file is input as stdin to SSH
@ -41,6 +48,7 @@ Values available for this section:
| `host` | If not specified, the command will execute locally. | `string` | no | | `host` | If not specified, the command will execute locally. | `string` | no |
| `scriptEnvFile` | When type is `scriptFile`, the script is appended to this file. | `string` | no | | `scriptEnvFile` | When type is `scriptFile`, the script is appended to this file. | `string` | no |
| `shell` | Only applicable when host is not specified | `string` | no | | `shell` | Only applicable when host is not specified | `string` | no |
| `hooks` | Hooks are used at the end of the individual command. Must be another command. | `[]string` | no |
#### cmd #### cmd
@ -93,9 +101,9 @@ Make sure to escape any shell input.
Path to a file. Path to a file.
When type is `scriptFile`, the script is appended to this file. When type is specified, the script is appended to this file.
This is useful for specifiing environment variables or other things so they don't have to be included in the script. This is useful for specifying environment variables or other things so they don't have to be included in the script.
### type ### type
@ -103,7 +111,7 @@ May be `scriptFile` or `script`. Runs script from local machine on remote host p
If `type` is `script`, `cmd` is used as the script. If `type` is `script`, `cmd` is used as the script.
If `type` is `scriptFile`, cmd must be a file path. If `type` is `scriptFile`, cmd must be a script file.
### environment ### environment
@ -116,3 +124,23 @@ For now, the variables have to be defined in an `.env` file in the same director
If using it with host specified, the SSH server has to be configured to accept those env variables. If using it with host specified, the SSH server has to be configured to accept those env variables.
If the command is run locally, the OS's environment is added. If the command is run locally, the OS's environment is added.
### hooks
Hooks are run after the command is run.
Errors are run if the command errors, success if it returns no error. Final hooks are run regardless of error condition.
Values for hooks are as follows:
```yaml
command:
hook:
# these commands are defined elsewhere in the file
error:
- errcommand
success:
- successcommand
final:
- donecommand
```

View File

@ -48,7 +48,7 @@ commands:
To execute groups of commands in sequence, use a list configuration. To execute groups of commands in sequence, use a list configuration.
```yaml ```yaml
cmd-configs: cmd-lists:
cmds-to-run: # this can be any name you want cmds-to-run: # this can be any name you want
# all commands have to be defined in the commands section # all commands have to be defined in the commands section
order: order:
@ -97,7 +97,7 @@ hosts:
The notifications object can have two forms. The notifications object can have two forms.
For more, [see the notification object documentation](/config/notifications). The top-level map key is id that has to be referenced by the `cmd-configs` key `notifications`. For more, [see the notification object documentation](/config/notifications). The top-level map key is id that has to be referenced by the `cmd-lists` key `notifications`.
```yaml ```yaml
notifications: notifications:

View File

@ -22,7 +22,7 @@ commands:
hostname: hostname:
cmd: hostname cmd: hostname
cmd-configs: cmd-lists:
cmds-to-run: # this can be any name you want cmds-to-run: # this can be any name you want
# all commands have to be defined # all commands have to be defined
order: order:

17
go.mod
View File

@ -15,7 +15,6 @@ require (
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/rs/zerolog v1.30.0 github.com/rs/zerolog v1.30.0
github.com/spf13/cobra v1.7.0 github.com/spf13/cobra v1.7.0
go.mongodb.org/mongo-driver v1.12.1
golang.org/x/crypto v0.13.0 golang.org/x/crypto v0.13.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
maunium.net/go/mautrix v0.16.0 maunium.net/go/mautrix v0.16.0
@ -25,10 +24,8 @@ require (
require ( require (
github.com/cenkalti/backoff/v3 v3.2.2 // indirect github.com/cenkalti/backoff/v3 v3.2.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.0 // indirect github.com/go-jose/go-jose/v3 v3.0.0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.3.1 // indirect github.com/google/uuid v1.3.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
@ -41,35 +38,22 @@ require (
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible // indirect github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/knadh/koanf/maps v0.1.1 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect
github.com/knadh/koanf/providers/structs v0.1.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.1 // indirect github.com/stretchr/objx v0.5.1 // indirect
github.com/stretchr/testify v1.8.4 // indirect github.com/stretchr/testify v1.8.4 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tidwall/gjson v1.16.0 // indirect github.com/tidwall/gjson v1.16.0 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect github.com/tidwall/sjson v1.2.5 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
go.mau.fi/util v0.0.0-20230906155759-14bad39a8718 // indirect go.mau.fi/util v0.0.0-20230906155759-14bad39a8718 // indirect
go.uber.org/atomic v1.11.0 // indirect go.uber.org/atomic v1.11.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
@ -78,7 +62,6 @@ require (
golang.org/x/sys v0.12.0 // indirect golang.org/x/sys v0.12.0 // indirect
golang.org/x/text v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.3.0 // indirect golang.org/x/time v0.3.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
maunium.net/go/maulogger/v2 v2.4.1 // indirect maunium.net/go/maulogger/v2 v2.4.1 // indirect
) )

48
go.sum
View File

@ -19,11 +19,7 @@ github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyM
github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@ -61,9 +57,6 @@ github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w=
@ -100,9 +93,6 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/nikoksr/notify v0.41.0 h1:4LGE41GpWdHX5M3Xo6DlWRwS2WLDbOq1Rk7IzY4vjmQ= github.com/nikoksr/notify v0.41.0 h1:4LGE41GpWdHX5M3Xo6DlWRwS2WLDbOq1Rk7IzY4vjmQ=
github.com/nikoksr/notify v0.41.0/go.mod h1:FoE0UVPeopz1Vy5nm9vQZ+JVmYjEIjQgbFstbkw+cRE= github.com/nikoksr/notify v0.41.0/go.mod h1:FoE0UVPeopz1Vy5nm9vQZ+JVmYjEIjQgbFstbkw+cRE=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
@ -151,76 +141,38 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mau.fi/util v0.0.0-20230906155759-14bad39a8718 h1:hmm5bZqE0M8+Uvys0HJPCSbAIZIwYtTkBKYPjAWHuMM= go.mau.fi/util v0.0.0-20230906155759-14bad39a8718 h1:hmm5bZqE0M8+Uvys0HJPCSbAIZIwYtTkBKYPjAWHuMM=
go.mau.fi/util v0.0.0-20230906155759-14bad39a8718/go.mod h1:AxuJUMCxpzgJ5eV9JbPWKRH8aAJJidxetNdUj7qcb84= go.mau.fi/util v0.0.0-20230906155759-14bad39a8718/go.mod h1:AxuJUMCxpzgJ5eV9JbPWKRH8aAJJidxetNdUj7qcb84=
go.mongodb.org/mongo-driver v1.12.1 h1:nLkghSU8fQNaK7oUmDhQFsnrtcoNy7Z6LVFKsEecqgE=
go.mongodb.org/mongo-driver v1.12.1/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -17,7 +17,6 @@ import (
"embed" "embed"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log"
) )
//go:embed templates/*.txt //go:embed templates/*.txt
@ -31,6 +30,8 @@ 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.
//
// Returns the output as a slice and an error, if any
func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([]string, error) { func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([]string, error) {
var ( var (
@ -49,13 +50,14 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
ArgsStr += fmt.Sprintf(" %s", v) ArgsStr += fmt.Sprintf(" %s", v)
} }
// is host defined
if command.Host != nil { if command.Host != nil {
command.Type = strings.TrimSpace(command.Type) command.Type = strings.TrimSpace(command.Type)
if command.Type != "" { if command.Type != "" {
cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running script %s on host %s", command.Cmd, *command.Host)).Send() cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running script %s on host %s", command.Name, *command.Host)).Send()
} else { } else {
cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s %s on host %s", command.Cmd, ArgsStr, *command.Host)).Send() cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s on host %s", command.Name, *command.Host)).Send()
} }
if command.RemoteHost.SshClient == nil { if command.RemoteHost.SshClient == nil {
@ -64,6 +66,8 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
return nil, err return nil, err
} }
} }
// create new ssh session
commandSession, err := command.RemoteHost.SshClient.NewSession() commandSession, err := command.RemoteHost.SshClient.NewSession()
// Retry connecting to host; if that fails, error. If it does not fail, try to create new session // Retry connecting to host; if that fails, error. If it does not fail, try to create new session
@ -86,6 +90,7 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
for _, a := range command.Args { for _, a := range command.Args {
cmd += " " + a cmd += " " + a
} }
cmdOutWriters = io.MultiWriter(&cmdOutBuf) cmdOutWriters = io.MultiWriter(&cmdOutBuf)
if IsCmdStdOutEnabled() { if IsCmdStdOutEnabled() {
@ -94,17 +99,64 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
commandSession.Stdout = cmdOutWriters commandSession.Stdout = cmdOutWriters
commandSession.Stderr = cmdOutWriters commandSession.Stderr = cmdOutWriters
// Is command type defined. That is, is it local or not
if command.Type != "" { if command.Type != "" {
// did the program panic while writing to the buffer? var (
script *bytes.Buffer
buffer bytes.Buffer
scriptEnvFileBuffer bytes.Buffer
scriptFileBuffer bytes.Buffer
dirErr error
scriptEnvFilePresent bool
)
defer func() { defer func() {
// did the program panic while writing to the buffer?
if err := recover(); err != nil { if err := recover(); err != nil {
cmdCtxLogger.Info().Msg(fmt.Sprintf("panic occured writing to buffer: %x", err)) // cmdCtxLogger.Debug().Msg(fmt.Sprintf("script buffer: %v", script))
cmdCtxLogger.Info().Msg(fmt.Sprintf("panic occurred writing to buffer: %v", err))
} }
}() }()
if command.Type == "script" { if command.Type == "script" {
script := bytes.NewBufferString(cmd + "\n")
if command.ScriptEnvFile != "" {
command.ScriptEnvFile, dirErr = resolveDir(command.ScriptEnvFile)
if dirErr != nil {
return nil, dirErr
}
file, err := os.Open(command.ScriptEnvFile)
if err != nil {
return nil, err
}
defer file.Close()
_, err = io.Copy(&scriptEnvFileBuffer, file)
if err != nil {
return nil, err
}
// Bug: writing to buffer triggers panic
// why?
// use bytes.Buffer instead of pointer to memory (*bytes.Buffer)
_, err = buffer.WriteString(scriptEnvFileBuffer.String())
if err != nil {
return nil, err
}
// write newline
buffer.WriteByte(0x0A)
_, err = buffer.WriteString(cmd)
if err != nil {
return nil, err
}
script = &buffer
} else {
script = bytes.NewBufferString(cmd + "\n")
}
commandSession.Stdin = script commandSession.Stdin = script
if err := commandSession.Shell(); err != nil { if err := commandSession.Shell(); err != nil {
@ -115,7 +167,7 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
outScanner := bufio.NewScanner(&cmdOutBuf) outScanner := bufio.NewScanner(&cmdOutBuf)
for outScanner.Scan() { for outScanner.Scan() {
outMap := make(map[string]interface{}) outMap := make(map[string]interface{})
outMap["cmd"] = cmd outMap["cmd"] = command.Name
outMap["output"] = outScanner.Text() outMap["output"] = outScanner.Text()
if str, ok := outMap["output"].(string); ok { if str, ok := outMap["output"].(string); ok {
outputArr = append(outputArr, str) outputArr = append(outputArr, str)
@ -128,25 +180,20 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
outScanner := bufio.NewScanner(&cmdOutBuf) outScanner := bufio.NewScanner(&cmdOutBuf)
for outScanner.Scan() { for outScanner.Scan() {
outMap := make(map[string]interface{}) outMap := make(map[string]interface{})
outMap["cmd"] = cmd outMap["cmd"] = command.Name
outMap["output"] = outScanner.Text() outMap["output"] = outScanner.Text()
if str, ok := outMap["output"].(string); ok { if str, ok := outMap["output"].(string); ok {
outputArr = append(outputArr, str) outputArr = append(outputArr, str)
} }
log.Info().Fields(outMap).Send() cmdCtxLogger.Info().Fields(outMap).Send()
} }
return outputArr, nil return outputArr, nil
} }
if command.Type == "scriptFile" { if command.Type == "scriptFile" {
var (
buffer bytes.Buffer
scriptEnvFileBuffer bytes.Buffer
scriptFileBuffer bytes.Buffer
dirErr error
scriptEnvFilePresent bool
)
if command.ScriptEnvFile != "" { if command.ScriptEnvFile != "" {
command.ScriptEnvFile, dirErr = resolveDir(command.ScriptEnvFile) command.ScriptEnvFile, dirErr = resolveDir(command.ScriptEnvFile)
if dirErr != nil { if dirErr != nil {
return nil, dirErr return nil, dirErr
@ -164,20 +211,27 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
} }
command.Cmd, dirErr = resolveDir(command.Cmd) command.Cmd, dirErr = resolveDir(command.Cmd)
if dirErr != nil { if dirErr != nil {
return nil, dirErr return nil, dirErr
} }
// treat command.Cmd as a file
file, err := os.Open(command.Cmd) file, err := os.Open(command.Cmd)
if err != nil { if err != nil {
return nil, err return nil, err
} }
_, err = io.Copy(&scriptFileBuffer, file) _, err = io.Copy(&scriptFileBuffer, file)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer file.Close() defer file.Close()
// append scriptEnvFile to scriptFileBuffer
if scriptEnvFilePresent { if scriptEnvFilePresent {
_, err := buffer.WriteString(scriptEnvFileBuffer.String()) _, err := buffer.WriteString(scriptEnvFileBuffer.String())
if err != nil { if err != nil {
@ -236,7 +290,7 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
outScanner := bufio.NewScanner(&cmdOutBuf) outScanner := bufio.NewScanner(&cmdOutBuf)
for outScanner.Scan() { for outScanner.Scan() {
outMap := make(map[string]interface{}) outMap := make(map[string]interface{})
outMap["cmd"] = cmd outMap["cmd"] = command.Name
outMap["output"] = outScanner.Text() outMap["output"] = outScanner.Text()
if str, ok := outMap["output"].(string); ok { if str, ok := outMap["output"].(string); ok {
outputArr = append(outputArr, str) outputArr = append(outputArr, str)
@ -245,14 +299,14 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
} }
if err != nil { if err != nil {
cmdCtxLogger.Error().Err(fmt.Errorf("error when running cmd: %s: %w", command.Cmd, err)).Send() cmdCtxLogger.Error().Err(fmt.Errorf("error when running cmd: %s: %w", command.Name, err)).Send()
return outputArr, err return outputArr, err
} }
} else { } else {
var err error var err error
if command.Shell != "" { if command.Shell != "" {
cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s %s on local machine in %s", command.Cmd, ArgsStr, command.Shell)).Send() cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s on local machine in %s", command.Name, command.Shell)).Send()
ArgsStr = fmt.Sprintf("%s %s", command.Cmd, ArgsStr) ArgsStr = fmt.Sprintf("%s %s", command.Cmd, ArgsStr)
@ -278,39 +332,44 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
for outScanner.Scan() { for outScanner.Scan() {
outMap := make(map[string]interface{}) outMap := make(map[string]interface{})
outMap["cmd"] = command.Cmd outMap["cmd"] = command.Name
outMap["output"] = outScanner.Text() outMap["output"] = outScanner.Text()
if str, ok := outMap["output"].(string); ok { if str, ok := outMap["output"].(string); ok {
outputArr = append(outputArr, str) outputArr = append(outputArr, str)
} }
log.Info().Fields(outMap).Send() cmdCtxLogger.Info().Fields(outMap).Send()
} }
if err != nil { if err != nil {
cmdCtxLogger.Error().Err(fmt.Errorf("error when running cmd %s: %w", command.Cmd, err)).Send() cmdCtxLogger.Error().Err(fmt.Errorf("error when running cmd %s: %w", command.Name, err)).Send()
return outputArr, err return outputArr, err
} }
return outputArr, nil return outputArr, nil
} }
cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s %s on local machine", command.Cmd, ArgsStr)).Send()
cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s on local machine", command.Name)).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, cmdCtxLogger) injectEnvIntoLocalCMD(envVars, localCMD, cmdCtxLogger)
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)
} }
localCMD.Stdout = cmdOutWriters localCMD.Stdout = cmdOutWriters
localCMD.Stderr = cmdOutWriters localCMD.Stderr = cmdOutWriters
err = localCMD.Run() err = localCMD.Run()
outScanner := bufio.NewScanner(&cmdOutBuf) outScanner := bufio.NewScanner(&cmdOutBuf)
for outScanner.Scan() { for outScanner.Scan() {
outMap := make(map[string]interface{}) outMap := make(map[string]interface{})
outMap["cmd"] = command.Cmd outMap["cmd"] = command.Cmd
@ -322,48 +381,44 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
cmdCtxLogger.Info().Fields(outMap).Send() cmdCtxLogger.Info().Fields(outMap).Send()
} }
if err != nil { if err != nil {
cmdCtxLogger.Error().Err(fmt.Errorf("error when running cmd %s: %w", command.Cmd, err)).Send() cmdCtxLogger.Error().Err(fmt.Errorf("error when running cmd %s: %w", command.Name, err)).Send()
return outputArr, err return outputArr, err
} }
} }
return outputArr, nil return outputArr, nil
} }
// cmdListWorker
func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<- string, opts *ConfigOpts) { func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<- string, opts *ConfigOpts) {
// iterate over list to run
res := CmdListResults{}
for list := range jobs { for list := range jobs {
fieldsMap := make(map[string]interface{}) fieldsMap := make(map[string]interface{})
fieldsMap["list"] = list.Name fieldsMap["list"] = list.Name
var cmdLogger zerolog.Logger
cmdLog := opts.Logger.Info() var count int // count of how many commands have been executed
var cmdsRan []string // store the commands that have been executed
var count int var outStructArr []outStruct // stores output messages
var cmdsRan []string
var outStructArr []outStruct
for _, cmd := range list.Order { for _, cmd := range list.Order {
currentCmd := opts.Cmds[cmd].Cmd
fieldsMap["cmd"] = opts.Cmds[cmd].Cmd currentCmd := opts.Cmds[cmd].Name
fieldsMap["cmd"] = opts.Cmds[cmd].Name
cmdToRun := opts.Cmds[cmd] cmdToRun := opts.Cmds[cmd]
cmdLog.Fields(fieldsMap).Send()
cmdLogger := opts.Logger.With(). cmdLogger = cmdToRun.generateLogger(opts)
Str("backy-cmd", cmd).Str("Host", "local machine"). cmdLogger.Info().Fields(fieldsMap).Send()
Logger()
if cmdToRun.Host != nil {
cmdLogger = opts.Logger.With().
Str("backy-cmd", cmd).Str("Host", *cmdToRun.Host).
Logger()
}
outputArr, runOutErr := cmdToRun.RunCmd(cmdLogger, opts) outputArr, runOutErr := cmdToRun.RunCmd(cmdLogger, opts)
if list.NotifyConfig != nil { if list.NotifyConfig != nil {
// check if the command output should be included
if cmdToRun.GetOutput || list.GetOutput { if cmdToRun.GetOutput || list.GetOutput {
outputStruct := outStruct{ outputStruct := outStruct{
CmdName: cmd, CmdName: cmdToRun.Name,
CmdExecuted: currentCmd, CmdExecuted: currentCmd,
Output: outputArr, Output: outputArr,
} }
@ -374,8 +429,9 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<-
} }
count++ count++
if runOutErr != nil { if runOutErr != nil {
var errMsg bytes.Buffer res.ErrCmd = cmd
if list.NotifyConfig != nil { if list.NotifyConfig != nil {
var errMsg bytes.Buffer
errStruct := make(map[string]interface{}) errStruct := make(map[string]interface{})
errStruct["listName"] = list.Name errStruct["listName"] = list.Name
@ -391,26 +447,29 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<-
tmpErr := msgTemps.err.Execute(&errMsg, errStruct) tmpErr := msgTemps.err.Execute(&errMsg, errStruct)
if tmpErr != nil { if tmpErr != nil {
opts.Logger.Err(tmpErr).Send() cmdLogger.Err(tmpErr).Send()
} }
notifySendErr := list.NotifyConfig.Send(context.Background(), fmt.Sprintf("List %s failed", list.Name), errMsg.String()) notifySendErr := list.NotifyConfig.Send(context.Background(), fmt.Sprintf("List %s failed", list.Name), errMsg.String())
if notifySendErr != nil { if notifySendErr != nil {
opts.Logger.Err(notifySendErr).Send() cmdLogger.Err(notifySendErr).Send()
} }
} }
opts.Logger.Err(runOutErr).Send() cmdLogger.Err(runOutErr).Send()
break break
} else { } else {
if count == len(list.Order) {
cmdsRan = append(cmdsRan, cmd) cmdsRan = append(cmdsRan, cmd)
if count == len(list.Order) {
var successMsg bytes.Buffer var successMsg bytes.Buffer
if list.NotifyConfig != nil { // if notification config is not nil, and NotifyOnSuccess is true or GetOuput is true,
// then send notification
if list.NotifyConfig != nil && (list.NotifyOnSuccess || list.GetOutput) {
successStruct := make(map[string]interface{}) successStruct := make(map[string]interface{})
successStruct["listName"] = list.Name successStruct["listName"] = list.Name
@ -421,23 +480,21 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<-
tmpErr := msgTemps.success.Execute(&successMsg, successStruct) tmpErr := msgTemps.success.Execute(&successMsg, successStruct)
if tmpErr != nil { if tmpErr != nil {
opts.Logger.Err(tmpErr).Send() cmdLogger.Err(tmpErr).Send()
break break
} }
err := list.NotifyConfig.Send(context.Background(), fmt.Sprintf("List %s succeded", list.Name), successMsg.String()) err := list.NotifyConfig.Send(context.Background(), fmt.Sprintf("List %s succeeded", list.Name), successMsg.String())
if err != nil { if err != nil {
opts.Logger.Err(err).Send() cmdLogger.Err(err).Send()
} }
} }
} else {
cmdsRan = append(cmdsRan, cmd)
} }
} }
} }
results <- "done" results <- res.ErrCmd
} }
} }
@ -473,7 +530,23 @@ func (opts *ConfigOpts) RunListConfig(cron string) {
close(listChan) close(listChan)
for a := 1; a <= configListsLen; a++ { for a := 1; a <= configListsLen; a++ {
<-results l := <-results
opts.Logger.Debug().Msg(l)
if l != "" {
// execute error hooks
opts.Logger.Debug().Msg("hooks are working")
opts.Cmds[l].ExecuteHooks("error", opts)
} else {
// execute success hooks
opts.Cmds[l].ExecuteHooks("success", opts)
}
// execute final hooks
opts.Cmds[l].ExecuteHooks("final", opts)
} }
opts.closeHostConnections() opts.closeHostConnections()
@ -482,13 +555,18 @@ func (opts *ConfigOpts) RunListConfig(cron string) {
func (config *ConfigOpts) ExecuteCmds(opts *ConfigOpts) { func (config *ConfigOpts) ExecuteCmds(opts *ConfigOpts) {
for _, cmd := range opts.executeCmds { for _, cmd := range opts.executeCmds {
cmdToRun := opts.Cmds[cmd] cmdToRun := opts.Cmds[cmd]
cmdLogger := opts.Logger.With(). cmdLogger := cmdToRun.generateLogger(opts)
Str("backy-cmd", cmd).
Logger()
_, runErr := cmdToRun.RunCmd(cmdLogger, opts) _, runErr := cmdToRun.RunCmd(cmdLogger, opts)
if runErr != nil { if runErr != nil {
opts.Logger.Err(runErr).Send() opts.Logger.Err(runErr).Send()
cmdToRun.ExecuteHooks("error", opts)
} else {
cmdToRun.ExecuteHooks("success", opts)
} }
cmdToRun.ExecuteHooks("final", opts)
} }
opts.closeHostConnections() opts.closeHostConnections()
@ -531,3 +609,50 @@ func (c *ConfigOpts) closeHostConnections() {
} }
} }
} }
func (cmd *Command) ExecuteHooks(hookType string, opts *ConfigOpts) {
if cmd.Hooks == nil {
return
}
switch hookType {
case "error":
for _, v := range cmd.Hooks.Error {
errCmd := opts.Cmds[v]
cmdLogger := opts.Logger.With().
Str("backy-cmd", v).
Logger()
errCmd.RunCmd(cmdLogger, opts)
}
case "success":
for _, v := range cmd.Hooks.Success {
successCmd := opts.Cmds[v]
cmdLogger := opts.Logger.With().
Str("backy-cmd", v).
Logger()
successCmd.RunCmd(cmdLogger, opts)
}
case "final":
for _, v := range cmd.Hooks.Final {
finalCmd := opts.Cmds[v]
cmdLogger := opts.Logger.With().
Str("backy-cmd", v).
Logger()
finalCmd.RunCmd(cmdLogger, opts)
}
}
}
func (cmd *Command) generateLogger(opts *ConfigOpts) zerolog.Logger {
cmdLogger := opts.Logger.With().
Str("backy-cmd", cmd.Name).Str("Host", "local machine").
Logger()
if cmd.Host != nil {
cmdLogger = opts.Logger.With().
Str("backy-cmd", cmd.Name).Str("Host", *cmd.Host).
Logger()
}
return cmdLogger
}

View File

@ -22,16 +22,28 @@ var homeDirErr error
var backyHomeConfDir string var backyHomeConfDir string
var configFiles []string var configFiles []string
const macroStart string = "%{"
const macroEnd string = "}%"
const envMacroStart string = "%{env:"
const vaultMacroStart string = "%{env:"
func (opts *ConfigOpts) InitConfig() { func (opts *ConfigOpts) InitConfig() {
homeDir, homeDirErr = os.UserHomeDir() homeDir, homeDirErr = os.UserHomeDir()
if homeDirErr != nil { if homeDirErr != nil {
fmt.Println(homeDirErr) fmt.Println(homeDirErr)
logging.ExitWithMSG(homeDirErr.Error(), 1, nil)
} }
backyHomeConfDir = homeDir + "/.config/backy/" backyHomeConfDir = homeDir + "/.config/backy/"
configFiles = []string{"./backy.yml", "./backy.yaml", backyHomeConfDir + "backy.yml", backyHomeConfDir + "backy.yaml"} configFiles = []string{"./backy.yml", "./backy.yaml", backyHomeConfDir + "backy.yml", backyHomeConfDir + "backy.yaml"}
backyKoanf := koanf.New(".") backyKoanf := koanf.New(".")
opts.ConfigFilePath = strings.TrimSpace(opts.ConfigFilePath) opts.ConfigFilePath = strings.TrimSpace(opts.ConfigFilePath)
if opts.ConfigFilePath != "" { if opts.ConfigFilePath != "" {
err := testFile(opts.ConfigFilePath) err := testFile(opts.ConfigFilePath)
if err != nil { if err != nil {
@ -43,16 +55,16 @@ func (opts *ConfigOpts) InitConfig() {
} }
} else { } else {
cFileFalures := 0 cFileFailures := 0
for _, c := range configFiles { for _, c := range configFiles {
if err := backyKoanf.Load(file.Provider(c), yaml.Parser()); err != nil { if err := backyKoanf.Load(file.Provider(c), yaml.Parser()); err != nil {
cFileFalures++ cFileFailures++
} else { } else {
opts.ConfigFilePath = c opts.ConfigFilePath = c
break break
} }
} }
if cFileFalures == len(configFiles) { if cFileFailures == len(configFiles) {
logging.ExitWithMSG(fmt.Sprintf("could not find a config file. Put one in the following paths: %v", configFiles), 1, &opts.Logger) logging.ExitWithMSG(fmt.Sprintf("could not find a config file. Put one in the following paths: %v", configFiles), 1, &opts.Logger)
} }
} }
@ -80,32 +92,43 @@ func ReadConfig(opts *ConfigOpts) *ConfigOpts {
} }
CheckConfigValues(backyKoanf, opts.ConfigFilePath) CheckConfigValues(backyKoanf, opts.ConfigFilePath)
// check for commands in file
for _, c := range opts.executeCmds { for _, c := range opts.executeCmds {
if !backyKoanf.Exists(getCmdFromConfig(c)) { if !backyKoanf.Exists(getCmdFromConfig(c)) {
logging.ExitWithMSG(Sprintf("command %s is not in config file %s", c, opts.ConfigFilePath), 1, nil) logging.ExitWithMSG(Sprintf("command %s is not in config file %s", c, opts.ConfigFilePath), 1, nil)
} }
} }
for _, l := range opts.executeLists { // TODO: refactor this further down the line
if !backyKoanf.Exists(getCmdListFromConfig(l)) {
logging.ExitWithMSG(Sprintf("list %s not found", l), 1, nil) // for _, l := range opts.executeLists {
} // if !backyKoanf.Exists(getCmdListFromConfig(l)) {
} // logging.ExitWithMSG(Sprintf("list %s not found", l), 1, nil)
// }
// }
// check for verbosity, via
// 1. config file
// 2. TODO: CLI flag
// 3. TODO: ENV var
var ( var (
verbose bool isLoggingVerbose bool
logFile string logFile string
) )
verbose = backyKoanf.Bool(getLoggingKeyFromConfig("verbose")) isLoggingVerbose = backyKoanf.Bool(getLoggingKeyFromConfig("verbose"))
logFile = fmt.Sprintf("%s/backy.log", path.Dir(opts.ConfigFilePath)) // get full path to logfile
logFile = fmt.Sprintf("%s/backy.log", path.Dir(opts.ConfigFilePath))
if backyKoanf.Exists(getLoggingKeyFromConfig("file")) { if backyKoanf.Exists(getLoggingKeyFromConfig("file")) {
logFile = backyKoanf.String(getLoggingKeyFromConfig("file")) logFile = backyKoanf.String(getLoggingKeyFromConfig("file"))
} }
zerolog.SetGlobalLevel(zerolog.InfoLevel) zerolog.SetGlobalLevel(zerolog.InfoLevel)
if verbose { if isLoggingVerbose {
zerolog.SetGlobalLevel(zerolog.DebugLevel) zerolog.SetGlobalLevel(zerolog.DebugLevel)
globalLvl := zerolog.GlobalLevel() globalLvl := zerolog.GlobalLevel()
os.Setenv("BACKY_LOGLEVEL", Sprintf("%v", globalLvl)) os.Setenv("BACKY_LOGLEVEL", Sprintf("%v", globalLvl))
@ -128,8 +151,11 @@ func ReadConfig(opts *ConfigOpts) *ConfigOpts {
log.Info().Str("config file", opts.ConfigFilePath).Send() log.Info().Str("config file", opts.ConfigFilePath).Send()
unmarshalErr := backyKoanf.UnmarshalWithConf("commands", &opts.Cmds, koanf.UnmarshalConf{Tag: "yaml"}) unmarshalErr := backyKoanf.UnmarshalWithConf("commands", &opts.Cmds, koanf.UnmarshalConf{Tag: "yaml"})
if unmarshalErr != nil { if unmarshalErr != nil {
panic(fmt.Errorf("error unmarshalling cmds struct: %w", unmarshalErr))
panic(fmt.Errorf("error unmarshaling cmds struct: %w", unmarshalErr))
} }
for cmdName, cmdConf := range opts.Cmds { for cmdName, cmdConf := range opts.Cmds {
@ -142,6 +168,8 @@ func ReadConfig(opts *ConfigOpts) *ConfigOpts {
expandEnvVars(opts.backyEnv, cmdConf.Environment) expandEnvVars(opts.backyEnv, cmdConf.Environment)
} }
// Get host configurations from config file
unmarshalErr = backyKoanf.UnmarshalWithConf("hosts", &opts.Hosts, koanf.UnmarshalConf{Tag: "yaml"}) unmarshalErr = backyKoanf.UnmarshalWithConf("hosts", &opts.Hosts, koanf.UnmarshalConf{Tag: "yaml"})
if unmarshalErr != nil { if unmarshalErr != nil {
panic(fmt.Errorf("error unmarshalling hosts struct: %w", unmarshalErr)) panic(fmt.Errorf("error unmarshalling hosts struct: %w", unmarshalErr))
@ -152,7 +180,6 @@ func ReadConfig(opts *ConfigOpts) *ConfigOpts {
} }
if host.ProxyJump != "" { if host.ProxyJump != "" {
proxyHosts := strings.Split(host.ProxyJump, ",") proxyHosts := strings.Split(host.ProxyJump, ",")
if len(proxyHosts) > 1 {
for hostNum, h := range proxyHosts { for hostNum, h := range proxyHosts {
if hostNum > 1 { if hostNum > 1 {
proxyHost, defined := opts.Hosts[h] proxyHost, defined := opts.Hosts[h]
@ -172,28 +199,78 @@ func ReadConfig(opts *ConfigOpts) *ConfigOpts {
} }
} }
} }
} else {
proxyHost, defined := opts.Hosts[proxyHosts[0]]
if defined {
host.ProxyHost = append(host.ProxyHost, proxyHost)
} else {
newProxy := &Host{Host: proxyHosts[0]}
host.ProxyHost = append(host.ProxyHost, newProxy)
}
}
} }
} }
if backyKoanf.Exists("cmd-lists") { // get command lists
unmarshalErr = backyKoanf.UnmarshalWithConf("cmd-lists", &opts.CmdConfigLists, koanf.UnmarshalConf{Tag: "yaml"}) // command lists should still be in the same file if no:
if unmarshalErr != nil { // 1. key 'cmd-lists.file' is found
logging.ExitWithMSG((fmt.Sprintf("error unmarshalling cmd list struct: %v", unmarshalErr)), 1, &opts.Logger) // 2. hosts.yml or hosts.yaml is found in the same directory as the backy config file
backyConfigFileDir := path.Dir(opts.ConfigFilePath)
listsConfig := koanf.New(".")
listConfigFiles := []string{path.Join(backyConfigFileDir, "lists.yml"), path.Join(backyConfigFileDir, "lists.yaml")}
log.Info().Strs("list config files", listConfigFiles).Send()
for _, l := range listConfigFiles {
cFileFailures := 0
if err := listsConfig.Load(file.Provider(l), yaml.Parser()); err != nil {
cFileFailures++
} else {
opts.ConfigFilePath = l
break
} }
if cFileFailures == len(configFiles) {
logging.ExitWithMSG(fmt.Sprintf("could not find a config file. Put one in the following paths: %v", listConfigFiles), 1, &opts.Logger)
// logging.ExitWithMSG((fmt.Sprintf("error unmarshalling cmd list struct: %v", unmarshalErr)), 1, &opts.Logger)
}
}
_ = listsConfig.UnmarshalWithConf("cmd-lists", &opts.CmdConfigLists, koanf.UnmarshalConf{Tag: "yaml"})
if backyKoanf.Exists("cmd-lists") {
unmarshalErr = backyKoanf.UnmarshalWithConf("cmd-lists", &opts.CmdConfigLists, koanf.UnmarshalConf{Tag: "yaml"})
// if unmarshalErr is not nil, look for a cmd-lists.file key
if unmarshalErr != nil {
// if file key exists, resolve file path and try to read and unmarshal file into command lists config
if backyKoanf.Exists("cmd-lists.file") {
opts.CmdListFile = strings.TrimSpace(backyKoanf.String("cmd-lists.file"))
cmdListFilePath := path.Clean(opts.CmdListFile)
// if path is not absolute, check config directory
if !strings.HasPrefix(cmdListFilePath, "/") {
opts.CmdListFile = path.Join(backyConfigFileDir, cmdListFilePath)
}
err := testFile(opts.CmdListFile)
if err != nil {
logging.ExitWithMSG(fmt.Sprintf("Could not open config file %s: %v. \n\nThe cmd-lists config should be in the main config file or should be in a lists.yml or lists.yaml file.", opts.CmdListFile, err), 1, nil)
}
if err := listsConfig.Load(file.Provider(opts.CmdListFile), yaml.Parser()); err != nil {
logging.ExitWithMSG(fmt.Sprintf("error loading config: %v", err), 1, &opts.Logger)
}
log.Info().Str("using lists config file", opts.CmdListFile).Send()
}
}
} }
var cmdNotFoundSliceErr []error var cmdNotFoundSliceErr []error
for cmdListName, cmdList := range opts.CmdConfigLists { for cmdListName, cmdList := range opts.CmdConfigLists {
if opts.useCron { if opts.cronEnabled {
cron := strings.TrimSpace(cmdList.Cron) cron := strings.TrimSpace(cmdList.Cron)
if cron == "" { if cron == "" {
delete(opts.CmdConfigLists, cmdListName) delete(opts.CmdConfigLists, cmdListName)
@ -209,19 +286,19 @@ func ReadConfig(opts *ConfigOpts) *ConfigOpts {
} }
} }
// Exit program if command is not found from list
if len(cmdNotFoundSliceErr) > 0 { if len(cmdNotFoundSliceErr) > 0 {
var cmdNotFoundErrorLog = log.Fatal() var cmdNotFoundErrorLog = log.Fatal()
cmdNotFoundErrorLog.Errs("commands not found", cmdNotFoundSliceErr).Send() cmdNotFoundErrorLog.Errs("commands not found", cmdNotFoundSliceErr).Send()
} }
if opts.useCron && (len(opts.CmdConfigLists) == 0) { if opts.cronEnabled && (len(opts.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)
} }
for c := range opts.Cmds { // process commands
if opts.executeCmds != nil && !contains(opts.executeCmds, c) { if err := processCmds(opts); err != nil {
delete(opts.Cmds, c) logging.ExitWithMSG(err.Error(), 1, &opts.Logger)
}
} }
if len(opts.executeLists) > 0 { if len(opts.executeLists) > 0 {
@ -240,23 +317,8 @@ func ReadConfig(opts *ConfigOpts) *ConfigOpts {
} }
} }
for _, cmd := range opts.Cmds {
if cmd.Host != nil {
host, hostFound := opts.Hosts[*cmd.Host]
if hostFound {
cmd.RemoteHost = host
cmd.RemoteHost.Host = host.Host
if host.HostName != "" {
cmd.RemoteHost.HostName = host.HostName
}
} else {
opts.Hosts[*cmd.Host] = &Host{Host: *cmd.Host}
cmd.RemoteHost = &Host{Host: *cmd.Host}
}
}
}
opts.SetupNotify() opts.SetupNotify()
if err := opts.setupVault(); err != nil { if err := opts.setupVault(); err != nil {
log.Err(err).Send() log.Err(err).Send()
} }
@ -383,3 +445,88 @@ func GetVaultKey(str string, opts *ConfigOpts, log zerolog.Logger) string {
} }
return value return value
} }
func processCmds(opts *ConfigOpts) error {
// process commands
for cmdName, cmd := range opts.Cmds {
if cmd.Name == "" {
cmd.Name = cmdName
}
// println("Cmd.Name = " + cmd.Name)
hooks := cmd.Hooks
// resolve hooks
if hooks != nil {
processHookSuccess := processHooks(cmd, hooks.Error, opts, "error")
if processHookSuccess != nil {
return processHookSuccess
}
processHookSuccess = processHooks(cmd, hooks.Success, opts, "success")
if processHookSuccess != nil {
return processHookSuccess
}
processHookSuccess = processHooks(cmd, hooks.Final, opts, "final")
if processHookSuccess != nil {
return processHookSuccess
}
}
// resolve hosts
if cmd.Host != nil {
host, hostFound := opts.Hosts[*cmd.Host]
if hostFound {
cmd.RemoteHost = host
cmd.RemoteHost.Host = host.Host
if host.HostName != "" {
cmd.RemoteHost.HostName = host.HostName
}
} else {
opts.Hosts[*cmd.Host] = &Host{Host: *cmd.Host}
cmd.RemoteHost = &Host{Host: *cmd.Host}
}
}
}
return nil
}
// processHooks evaluates if hooks are valid Commands
//
// Takes the following arguments:
//
// 1. a []string of hooks
// 2. a map of Commands as arguments
// 3. a string hookType, must be the hook type
//
// The cmds.hookRef is modified in this function.
//
// Returns the following:
//
// An error, if any, if the command is not found
func processHooks(cmd *Command, hooks []string, opts *ConfigOpts, hookType string) error {
// initialize hook type
var hookCmdFound bool
cmd.hookRefs = map[string]map[string]*Command{}
cmd.hookRefs[hookType] = map[string]*Command{}
for _, hook := range hooks {
var hookCmd *Command
// TODO: match by Command.Name
hookCmd, hookCmdFound = opts.Cmds[hook]
if !hookCmdFound {
return fmt.Errorf("error in command %s hook %s list: command %s not found", cmd.Name, hookType, hook)
}
cmd.hookRefs[hookType][hook] = hookCmd
// Recursive, decide if this is good
// if hookCmd.hookRefs == nil {
// }
// hookRef[hookType][h] = hookCmd
}
return nil
}

View File

@ -1,5 +1,68 @@
package backy package backy
func (opts *ConfigOpts) ListConfiguration() { import "fmt"
/*
Command: command [args...]
Host: Local or remote (list the name)
List: name
Commands:
flags: list commands
if listcommands: (use list command)
Command: command [args...]
Host: Local or remote (list the name)
*/
// ListCommand searches the commands in the file to find one
func (opts *ConfigOpts) ListCommand(cmd string) {
// bool for commands not found
// gets set to false if a command is not found
// set to true if the command is found
var cmdFound bool = false
var cmdInfo *Command
// check commands in file against cmd
for _, cmdInFile := range opts.executeCmds {
print(cmdInFile)
cmdFound = false
if cmd == cmdInFile {
cmdFound = true
cmdInfo = opts.Cmds[cmd]
break
}
}
// print the command's information
if cmdFound {
print("Command: ")
print(cmdInfo.Cmd)
if len(cmdInfo.Args) >= 0 {
for _, v := range cmdInfo.Args {
print(" ") // print space between command and args
print(v) // print command arg
}
}
// is is remote or local
if cmdInfo.Host != nil {
print("Host: ", cmdInfo.Host)
} else {
print("Host: Runs on Local Machine\n\n")
}
} else {
fmt.Printf("Command %s not found. Check spelling.\n", cmd)
}
} }

View File

@ -1,95 +0,0 @@
package backy
import (
"context"
"errors"
"fmt"
"os"
"time"
"github.com/joho/godotenv"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/mongo/readpref"
)
const mongoConfigKey = "global.mongo"
func (opts *ConfigOpts) InitMongo() {
if !opts.koanf.Bool(getMongoConfigKey("enabled")) {
return
}
var (
err error
client *mongo.Client
)
// TODO: Get uri and creditials from config
host := opts.koanf.String(getMongoConfigKey("host"))
port := opts.koanf.Int64(getMongoConfigKey("port"))
ctx, ctxCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer ctxCancel()
client, err = mongo.Connect(ctx, options.Client().ApplyURI(fmt.Sprintf("mongo://%s:%d", host, port)))
if opts.koanf.Bool(getMongoConfigKey("prod")) {
mongoEnvFileSet := opts.koanf.Exists(getMongoConfigKey("env"))
if mongoEnvFileSet {
getMongoConfigFromEnv(opts)
}
auth := options.Credential{}
auth.Password = opts.koanf.String("global.mongo.password")
auth.Username = opts.koanf.String("global.mongo.username")
client, err = mongo.Connect(ctx, options.Client().SetAuth(auth).ApplyURI("mongodb://localhost:27017"))
}
if err != nil {
opts.Logger.Fatal().Err(err).Send()
}
if err != nil {
opts.Logger.Fatal().Err(err).Send()
}
defer client.Disconnect(ctx)
err = client.Ping(ctx, readpref.Primary())
if err != nil {
opts.Logger.Fatal().Err(err).Send()
}
databases, err := client.ListDatabaseNames(ctx, bson.M{})
if err != nil {
opts.Logger.Fatal().Err(err).Send()
}
fmt.Println(databases)
backyDB := client.Database("backy")
backyDB.CreateCollection(context.Background(), "cmds")
backyDB.CreateCollection(context.Background(), "cmd-lists")
backyDB.CreateCollection(context.Background(), "logs")
opts.DB = backyDB
}
func getMongoConfigFromEnv(opts *ConfigOpts) error {
mongoEnvFile, err := os.Open(opts.koanf.String("global.mongo.env"))
if err != nil {
return err
}
mongoMap, mongoErr := godotenv.Parse(mongoEnvFile)
if mongoErr != nil {
return err
}
mongoPW, mongoPWFound := mongoMap["MONGO_PASSWORD"]
if !mongoPWFound {
return errors.New("MONGO_PASSWORD not set in " + mongoEnvFile.Name())
}
mongoUser, mongoUserFound := mongoMap["MONGO_USER"]
if !mongoUserFound {
return errors.New("MONGO_PASSWORD not set in " + mongoEnvFile.Name())
}
opts.koanf.Set(mongoConfigKey+".password", mongoPW)
opts.koanf.Set(mongoConfigKey+".username", mongoUser)
return nil
}
func getMongoConfigKey(key string) string {
return fmt.Sprintf("global.mongo.%s", key)
}

View File

@ -39,6 +39,7 @@ func (opts *ConfigOpts) SetupNotify() {
} }
for confName, cmdConfig := range opts.CmdConfigLists { for confName, cmdConfig := range opts.CmdConfigLists {
var services []notify.Notifier var services []notify.Notifier
for _, id := range cmdConfig.Notifications { for _, id := range cmdConfig.Notifications {
if !strings.Contains(id, ".") { if !strings.Contains(id, ".") {

View File

@ -20,7 +20,7 @@ import (
"golang.org/x/crypto/ssh/knownhosts" "golang.org/x/crypto/ssh/knownhosts"
) )
var PrivateKeyExtraInfoErr = errors.New("Private key may be encrypted. \nIf encrypted, make sure the password is specified correctly in the correct section: \n privatekeypassword: env:PR_KEY_PASS \n privatekeypassword: file:/path/to/password-file \n privatekeypassword: password (not recommended). \n ") var PrivateKeyExtraInfoErr = errors.New("Private key may be encrypted. \nIf encrypted, make sure the password is specified correctly in the correct section. This may be done in one of three ways: \n privatekeypassword: env:PR_KEY_PASS \n privatekeypassword: file:/path/to/password-file \n privatekeypassword: password (not recommended). \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
@ -30,22 +30,26 @@ var TS = strings.TrimSpace
// If any value is not found, defaults are used // If any value is not found, defaults are used
func (remoteConfig *Host) ConnectToSSHHost(opts *ConfigOpts) error { func (remoteConfig *Host) ConnectToSSHHost(opts *ConfigOpts) error {
// var sshClient *ssh.Client
var connectErr error var connectErr error
if TS(remoteConfig.ConfigFilePath) == "" { if TS(remoteConfig.ConfigFilePath) == "" {
remoteConfig.useDefaultConfig = true remoteConfig.useDefaultConfig = true
} }
khPath, khPathErr := GetKnownHosts(remoteConfig.KnownHostsFile)
khPathErr := remoteConfig.GetKnownHosts()
if khPathErr != nil { if khPathErr != nil {
return khPathErr return khPathErr
} }
if remoteConfig.ClientConfig == nil { if remoteConfig.ClientConfig == nil {
remoteConfig.ClientConfig = &ssh.ClientConfig{} remoteConfig.ClientConfig = &ssh.ClientConfig{}
} }
var configFile *os.File var configFile *os.File
var sshConfigFileOpenErr error var sshConfigFileOpenErr error
if !remoteConfig.useDefaultConfig { if !remoteConfig.useDefaultConfig {
var err error var err error
remoteConfig.ConfigFilePath, err = resolveDir(remoteConfig.ConfigFilePath) remoteConfig.ConfigFilePath, err = resolveDir(remoteConfig.ConfigFilePath)
@ -72,9 +76,11 @@ func (remoteConfig *Host) ConnectToSSHHost(opts *ConfigOpts) error {
} }
err := remoteConfig.GetProxyJumpFromConfig(opts.Hosts) err := remoteConfig.GetProxyJumpFromConfig(opts.Hosts)
if err != nil { if err != nil {
return err return err
} }
if remoteConfig.ProxyHost != nil { if remoteConfig.ProxyHost != nil {
for _, proxyHost := range remoteConfig.ProxyHost { for _, proxyHost := range remoteConfig.ProxyHost {
err := proxyHost.GetProxyJumpConfig(opts.Hosts, opts) err := proxyHost.GetProxyJumpConfig(opts.Hosts, opts)
@ -86,10 +92,15 @@ func (remoteConfig *Host) ConnectToSSHHost(opts *ConfigOpts) error {
} }
remoteConfig.ClientConfig.Timeout = time.Second * 30 remoteConfig.ClientConfig.Timeout = time.Second * 30
remoteConfig.GetPrivateKeyFileFromConfig() remoteConfig.GetPrivateKeyFileFromConfig()
remoteConfig.GetPort() remoteConfig.GetPort()
remoteConfig.GetHostName() remoteConfig.GetHostName()
remoteConfig.CombineHostNameWithPort() remoteConfig.CombineHostNameWithPort()
remoteConfig.GetSshUserFromConfig() remoteConfig.GetSshUserFromConfig()
if remoteConfig.HostName == "" { if remoteConfig.HostName == "" {
@ -101,7 +112,7 @@ func (remoteConfig *Host) ConnectToSSHHost(opts *ConfigOpts) error {
return err return err
} }
hostKeyCallback, err := knownhosts.New(khPath) hostKeyCallback, err := knownhosts.New(remoteConfig.KnownHostsFile)
if err != nil { if err != nil {
return errors.Wrap(err, "could not create hostkeycallback function") return errors.Wrap(err, "could not create hostkeycallback function")
} }
@ -128,12 +139,19 @@ func (remoteConfig *Host) ConnectToSSHHost(opts *ConfigOpts) error {
} }
func (remoteHost *Host) GetSshUserFromConfig() { func (remoteHost *Host) GetSshUserFromConfig() {
if TS(remoteHost.User) == "" { if TS(remoteHost.User) == "" {
remoteHost.User, _ = remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "User") remoteHost.User, _ = remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "User")
if TS(remoteHost.User) == "" { if TS(remoteHost.User) == "" {
remoteHost.User = remoteHost.SSHConfigFile.DefaultUserSettings.Get(remoteHost.Host, "User") remoteHost.User = remoteHost.SSHConfigFile.DefaultUserSettings.Get(remoteHost.Host, "User")
if TS(remoteHost.User) == "" { if TS(remoteHost.User) == "" {
currentUser, _ := user.Current() currentUser, _ := user.Current()
remoteHost.User = currentUser.Username remoteHost.User = currentUser.Username
} }
} }
@ -145,39 +163,60 @@ func (remoteHost *Host) GetAuthMethods(opts *ConfigOpts) error {
var signer ssh.Signer var signer ssh.Signer
var err error var err error
var privateKey []byte var privateKey []byte
remoteHost.Password = strings.TrimSpace(remoteHost.Password) remoteHost.Password = strings.TrimSpace(remoteHost.Password)
remoteHost.PrivateKeyPassword = strings.TrimSpace(remoteHost.PrivateKeyPassword) remoteHost.PrivateKeyPassword = strings.TrimSpace(remoteHost.PrivateKeyPassword)
remoteHost.PrivateKeyPath = strings.TrimSpace(remoteHost.PrivateKeyPath) remoteHost.PrivateKeyPath = strings.TrimSpace(remoteHost.PrivateKeyPath)
if remoteHost.PrivateKeyPath != "" { if remoteHost.PrivateKeyPath != "" {
privateKey, err = os.ReadFile(remoteHost.PrivateKeyPath) privateKey, err = os.ReadFile(remoteHost.PrivateKeyPath)
if err != nil { if err != nil {
return err return err
} }
remoteHost.PrivateKeyPassword, err = GetPrivateKeyPassword(remoteHost.PrivateKeyPassword, opts, opts.Logger) remoteHost.PrivateKeyPassword, err = GetPrivateKeyPassword(remoteHost.PrivateKeyPassword, opts, opts.Logger)
if err != nil { if err != nil {
return err return err
} }
if remoteHost.PrivateKeyPassword == "" { if remoteHost.PrivateKeyPassword == "" {
signer, err = ssh.ParsePrivateKey(privateKey) signer, err = ssh.ParsePrivateKey(privateKey)
if err != nil { if err != nil {
return errors.Errorf("Failed to open private key file %s: %v \n\n %v", remoteHost.PrivateKeyPath, err, PrivateKeyExtraInfoErr) 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 errors.Errorf("Failed to open private key file %s: %v \n\n %v", remoteHost.PrivateKeyPath, err, PrivateKeyExtraInfoErr) 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)}
} }
} }
if remoteHost.Password == "" { if remoteHost.Password == "" {
remoteHost.Password, err = GetPassword(remoteHost.Password, opts, opts.Logger) remoteHost.Password, err = GetPassword(remoteHost.Password, opts, opts.Logger)
if err != nil { if err != nil {
return err return err
} }
remoteHost.ClientConfig.Auth = append(remoteHost.ClientConfig.Auth, ssh.Password(remoteHost.Password)) remoteHost.ClientConfig.Auth = append(remoteHost.ClientConfig.Auth, ssh.Password(remoteHost.Password))
} }
return nil return nil
} }
@ -209,10 +248,17 @@ func (remoteHost *Host) GetPrivateKeyFileFromConfig() {
func (remoteHost *Host) GetPort() { func (remoteHost *Host) GetPort() {
port := fmt.Sprintf("%d", remoteHost.Port) port := fmt.Sprintf("%d", remoteHost.Port)
// port specifed? // port specifed?
// port will be 0 if missing from backy config
if port == "0" { if port == "0" {
// get port from specified SSH config file
port, _ = remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "Port") port, _ = remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "Port")
if port == "" { if port == "" {
// get port from default SSH config file
port = remoteHost.SSHConfigFile.DefaultUserSettings.Get(remoteHost.Host, "Port") port = remoteHost.SSHConfigFile.DefaultUserSettings.Get(remoteHost.Host, "Port")
// set port to be default
if port == "" { if port == "" {
port = "22" port = "22"
} }
@ -223,10 +269,12 @@ func (remoteHost *Host) GetPort() {
} }
func (remoteHost *Host) CombineHostNameWithPort() { func (remoteHost *Host) CombineHostNameWithPort() {
port := fmt.Sprintf(":%d", remoteHost.Port)
if strings.HasSuffix(remoteHost.HostName, port) { // if the port is already in the HostName, leave it
if strings.HasSuffix(remoteHost.HostName, fmt.Sprintf(":%d", remoteHost.Port)) {
return return
} }
remoteHost.HostName = fmt.Sprintf("%s:%d", remoteHost.HostName, remoteHost.Port) remoteHost.HostName = fmt.Sprintf("%s:%d", remoteHost.HostName, remoteHost.Port)
} }
@ -265,18 +313,22 @@ func (remoteHost *Host) ConnectThroughBastion(log zerolog.Logger) (*ssh.Client,
return nil, err return nil, err
} }
sClient := ssh.NewClient(ncc, chans, reqs)
// sClient is an ssh client connected to the service host, through the bastion host. // sClient is an ssh client connected to the service host, through the bastion host.
sClient := ssh.NewClient(ncc, chans, reqs)
return sClient, nil return sClient, nil
} }
func GetKnownHosts(khPath string) (string, error) { // GetKnownHosts resolves the host's KnownHosts file if it is defined
// if not defined, the default location for this file is used
if TS(khPath) != "" { func (remotehHost *Host) GetKnownHosts() error {
return resolveDir(khPath) var knownHostsFileErr error
if TS(remotehHost.KnownHostsFile) != "" {
remotehHost.KnownHostsFile, knownHostsFileErr = resolveDir(remotehHost.KnownHostsFile)
return knownHostsFileErr
} }
return resolveDir("~/.ssh/known_hosts") remotehHost.KnownHostsFile, knownHostsFileErr = resolveDir("~/.ssh/known_hosts")
return knownHostsFileErr
} }
func GetPrivateKeyPassword(key string, opts *ConfigOpts, log zerolog.Logger) (string, error) { func GetPrivateKeyPassword(key string, opts *ConfigOpts, log zerolog.Logger) (string, error) {
@ -306,6 +358,7 @@ func GetPrivateKeyPassword(key string, opts *ConfigOpts, log zerolog.Logger) (st
return prKeyPassword, nil return prKeyPassword, nil
} }
// GetPassword gets any password
func GetPassword(pass string, opts *ConfigOpts, log zerolog.Logger) (string, error) { func GetPassword(pass string, opts *ConfigOpts, log zerolog.Logger) (string, error) {
pass = strings.TrimSpace(pass) pass = strings.TrimSpace(pass)
@ -370,7 +423,7 @@ func (remoteConfig *Host) GetProxyJumpConfig(hosts map[string]*Host, opts *Confi
remoteConfig.useDefaultConfig = true remoteConfig.useDefaultConfig = true
} }
khPath, khPathErr := GetKnownHosts(remoteConfig.KnownHostsFile) khPathErr := remoteConfig.GetKnownHosts()
if khPathErr != nil { if khPathErr != nil {
return khPathErr return khPathErr
@ -415,7 +468,7 @@ func (remoteConfig *Host) GetProxyJumpConfig(hosts map[string]*Host, opts *Confi
} }
// TODO: Add value/option to config for host key and add bool to check for host key // 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(remoteConfig.KnownHostsFile)
if err != nil { if err != nil {
return fmt.Errorf("could not create hostkeycallback function: %v", err) return fmt.Errorf("could not create hostkeycallback function: %v", err)
} }

View File

@ -15,7 +15,7 @@ The following commands ran:
{{end}} {{end}}
{{ end }} {{ end }}
{{ if .CmdOutput }}{{- range .CmdOutput }}Commad output for {{ .CmdName }}: {{ if .CmdOutput }}{{- range .CmdOutput }}Command output for {{ .CmdName }}:
{{- range .Output}} {{- range .Output}}
{{ . }} {{ . }}
{{ end }}{{ end }} {{ end }}{{ end }}

View File

@ -5,7 +5,7 @@ The following commands ran:
- {{. -}} - {{. -}}
{{end}} {{end}}
{{ if .CmdOutput }}{{- range .CmdOutput }}Commad output for {{ .CmdName }}: {{ if .CmdOutput }}{{- range .CmdOutput }}Command output for {{ .CmdName }}:
{{- range .Output}} {{- range .Output}}
{{ . }} {{ . }}
{{ end }}{{ end }} {{ end }}{{ end }}

View File

@ -9,30 +9,10 @@ import (
"github.com/knadh/koanf/v2" "github.com/knadh/koanf/v2"
"github.com/nikoksr/notify" "github.com/nikoksr/notify"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
type ( type (
CmdConfigSchema struct {
ID primitive.ObjectID `bson:"_id,omitempty"`
CmdList []string `bson:"command-list,omitempty"`
Name string `bson:"name,omitempty"`
}
CmdSchema struct {
ID primitive.ObjectID `bson:"_id,omitempty"`
Cmd string `bson:"cmd,omitempty"`
Args []string `bson:"args,omitempty"`
Host string `bson:"host,omitempty"`
Dir string `bson:"dir,omitempty"`
}
Schemas struct {
CmdConfigSchema
CmdSchema
}
// Host defines a host to which to connect. // Host defines a host to which to connect.
// If not provided, the values will be looked up in the default ssh config files // If not provided, the values will be looked up in the default ssh config files
@ -63,17 +43,24 @@ type (
} }
Command struct { Command struct {
Name string `yaml:"name,omitempty"`
// command to run // command to run
Cmd string `yaml:"cmd"` Cmd string `yaml:"cmd"`
// Possible values: script, scriptFile // Possible values: script, scriptFile
// If blank, it is regualar command. // If blank, it is regular command.
Type string `yaml:"type"` Type string `yaml:"type,omitempty"`
// host on which to run cmd // host on which to run cmd
Host *string `yaml:"host,omitempty"` Host *string `yaml:"host,omitempty"`
// Hooks are for running commands on certain events
Hooks *Hooks `yaml:"hooks,omitempty"`
// hook refs are internal references of commands for each hook type
hookRefs map[string]map[string]*Command
/* /*
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.
@ -109,9 +96,12 @@ 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"`
RunCmdOnFailure string `yaml:"runCmdOnFailure,omitempty"`
Order []string `yaml:"order,omitempty"` Order []string `yaml:"order,omitempty"`
Notifications []string `yaml:"notifications,omitempty"` Notifications []string `yaml:"notifications,omitempty"`
GetOutput bool `yaml:"getOutput,omitempty"` GetOutput bool `yaml:"getOutput,omitempty"`
NotifyOnSuccess bool `yaml:"notifyOnSuccess,omitempty"`
NotifyConfig *notify.Notify NotifyConfig *notify.Notify
} }
@ -136,11 +126,11 @@ type (
// Holds config file // Holds config file
ConfigFilePath string ConfigFilePath string
Schemas // for command list file
CmdListFile string
DB *mongo.Database
// use command lists using cron // use command lists using cron
useCron bool cronEnabled bool
// Holds commands to execute for the exec command // Holds commands to execute for the exec command
executeCmds []string executeCmds []string
// Holds lists to execute for the backup command // Holds lists to execute for the backup command
@ -205,4 +195,17 @@ type (
Commands []string Commands []string
Hosts []string Hosts []string
} }
Hooks struct {
Error []string `yaml:"error,omitempty"`
Success []string `yaml:"success,omitempty"`
Final []string `yaml:"final,omitempty"`
}
CmdListResults struct {
// name of the list
ListName string
// command that caused the list to fail
ErrCmd string
}
) )

View File

@ -56,10 +56,10 @@ func SetCmdsToSearch(cmds []string) BackyOptionFunc {
} }
} }
// UseCron enables the execution of command lists at specified times // cronEnabled enables the execution of command lists at specified times
func UseCron() BackyOptionFunc { func CronEnabled() BackyOptionFunc {
return func(bco *ConfigOpts) { return func(bco *ConfigOpts) {
bco.useCron = true bco.cronEnabled = true
} }
} }
@ -197,6 +197,7 @@ func resolveDir(path string) (string, error) {
return path, nil return path, nil
} }
// loadEnv loads a .env file from the config file directory
func (opts *ConfigOpts) loadEnv() { func (opts *ConfigOpts) loadEnv() {
envFileInConfigDir := fmt.Sprintf("%s/.env", path.Dir(opts.ConfigFilePath)) envFileInConfigDir := fmt.Sprintf("%s/.env", path.Dir(opts.ConfigFilePath))
var backyEnv map[string]string var backyEnv map[string]string
@ -208,6 +209,7 @@ func (opts *ConfigOpts) loadEnv() {
opts.backyEnv = backyEnv opts.backyEnv = backyEnv
} }
// expandEnvVars expands environment variables with the env used in the config
func expandEnvVars(backyEnv map[string]string, envVars []string) { func expandEnvVars(backyEnv map[string]string, envVars []string) {
env := func(name string) string { env := func(name string) string {
@ -219,10 +221,15 @@ func expandEnvVars(backyEnv map[string]string, envVars []string) {
return "" return ""
} }
// parse env variables using new macros
for indx, v := range envVars { for indx, v := range envVars {
if strings.Contains(v, "$") || (strings.Contains(v, "${") && strings.Contains(v, "}")) { if strings.HasPrefix(v, macroStart) && strings.HasSuffix(v, macroEnd) {
if strings.HasPrefix(v, envMacroStart) {
v = strings.TrimPrefix(v, envMacroStart)
v = strings.TrimRight(v, macroEnd)
out, _ := shell.Expand(v, env) out, _ := shell.Expand(v, env)
envVars[indx] = out envVars[indx] = out
} }
} }
}
} }