51 Commits

Author SHA1 Message Date
c483a1056f v0.8.0
Some checks failed
ci/woodpecker/push/publish-docs Pipeline was successful
ci/woodpecker/tag/gitea Pipeline was successful
ci/woodpecker/tag/publish-docs Pipeline was successful
ci/woodpecker/push/go-lint Pipeline failed
ci/woodpecker/release/publish-docs Pipeline was successful
2025-02-15 23:00:10 -06:00
3b9f569310 CI config changed
Some checks failed
ci/woodpecker/push/publish-docs Pipeline failed
2025-02-15 22:54:11 -06:00
843be7968b CI config changed 2025-02-15 22:50:44 -06:00
d477d850ac CI config changed 2025-02-15 22:46:49 -06:00
8eb3229af7 v0.8.0 2025-02-15 22:29:19 -06:00
d89a208bbd v0.8.0 2025-02-15 22:28:01 -06:00
0d28d6afcf breaking changes to keys 2025-02-15 22:27:11 -06:00
7c42a9a7cd v0.7.8
Some checks failed
ci/woodpecker/push/publish-docs Pipeline was successful
ci/woodpecker/tag/publish-docs Pipeline was successful
ci/woodpecker/tag/gitea Pipeline failed
2025-02-14 14:46:10 -06:00
31339fb4d8 v0.7.7
All checks were successful
ci/woodpecker/push/publish-docs Pipeline was successful
ci/woodpecker/tag/gitea Pipeline was successful
ci/woodpecker/tag/publish-docs Pipeline was successful
2025-02-14 14:31:24 -06:00
c642e827f5 v0.7.6
Some checks failed
ci/woodpecker/push/publish-docs Pipeline was successful
ci/woodpecker/tag/gitea Pipeline failed
ci/woodpecker/tag/publish-docs Pipeline was successful
2025-02-14 14:18:02 -06:00
a328239021 v0.7.5
Some checks failed
ci/woodpecker/push/publish-docs Pipeline was successful
ci/woodpecker/tag/gitea Pipeline failed
ci/woodpecker/tag/publish-docs Pipeline was successful
2025-02-14 13:39:18 -06:00
4ee60184bf v0.7.4
Some checks failed
ci/woodpecker/push/publish-docs Pipeline was successful
ci/woodpecker/tag/gitea Pipeline failed
ci/woodpecker/tag/publish-docs Pipeline was successful
2025-02-14 13:11:08 -06:00
161ad31577 v0.7.3
Some checks failed
ci/woodpecker/push/publish-docs Pipeline was successful
ci/woodpecker/tag/gitea Pipeline failed
ci/woodpecker/tag/publish-docs Pipeline was successful
2025-02-14 12:54:05 -06:00
7c5f4a95da v0.7.2
Some checks failed
ci/woodpecker/push/publish-docs Pipeline was successful
ci/woodpecker/tag/gitea Pipeline failed
ci/woodpecker/tag/publish-docs Pipeline was successful
2025-02-14 12:21:21 -06:00
4981acbf9d update CI configs 2025-02-14 12:19:21 -06:00
932d5c380f v0.7.1
Some checks failed
ci/woodpecker/push/publish-docs Pipeline was successful
ci/woodpecker/tag/gitea Pipeline failed
ci/woodpecker/tag/publish-docs Pipeline was successful
2025-02-14 11:56:46 -06:00
f84d76badf updat GitHube workflow file
All checks were successful
ci/woodpecker/push/publish-docs Pipeline was successful
2025-02-11 23:05:08 -06:00
6ee6e10621 v0.7.0 update docs
All checks were successful
ci/woodpecker/push/publish-docs Pipeline was successful
2025-02-11 22:00:10 -06:00
127d38c076 v0.7.0 make changes to release script 2025-02-11 21:48:40 -06:00
0218dee76d v0.7.0 change GoReleaser file
Some checks failed
ci/woodpecker/push/publish-docs Pipeline failed
2025-02-11 21:47:23 -06:00
67a1eab908 v0.7.0 make changes to release script
Some checks failed
ci/woodpecker/push/publish-docs Pipeline failed
ci/woodpecker/tag/gitea Pipeline was successful
ci/woodpecker/tag/publish-docs Pipeline failed
2025-02-11 21:37:42 -06:00
c618ca33f8 v0.7.0 make changes to release script
Some checks failed
ci/woodpecker/push/publish-docs Pipeline failed
ci/woodpecker/tag/publish-docs Pipeline is pending
ci/woodpecker/tag/gitea Pipeline failed
2025-02-11 21:30:13 -06:00
6e7d912fa2 v0.7.0 bump version
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed
ci/woodpecker/tag/publish-docs Pipeline is pending
ci/woodpecker/tag/gitea Pipeline failed
ci/woodpecker/push/publish-docs Pipeline failed
2025-02-11 21:19:57 -06:00
b90d1958b2 [WIP] v0.7.0 fix comments
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed
2025-02-11 12:19:09 -06:00
c187fbb735 [WIP] v0.7.0 almost ready to release
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed
2025-02-10 17:00:02 -06:00
c3de4386ab [WIP] v0.7.0 almost ready to release
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed
2025-02-10 16:55:54 -06:00
e20141043c Merge branch 'remoteResources' into develop
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed
2025-02-08 16:14:37 -06:00
11ec1a98d8 [WIP] v0.7.0 almost ready to release 2025-02-08 15:17:34 -06:00
8788d473a5 [WIP] v0.7.0 fixes and changes to cache and remotefetcher 2025-01-28 15:45:06 -06:00
edc669b340 [WIP] v0.7.0 fixes and changes to cache and remotefetcher 2025-01-28 15:43:57 -06:00
086835453b [WIP] v0.7.0 fixes and changes to cache and remotefetcher 2025-01-28 15:42:50 -06:00
5d3c265ce9 add comments
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed
2025-01-23 16:47:43 -06:00
8c633fd4b2 [WIP] v0.7.0 getCommandType first
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed
2025-01-15 23:33:55 -06:00
a664edaed7 [WIP] v0.7.0 run go mod tidy
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed
2025-01-14 19:14:23 -06:00
e88773e289 [WIP] v0.7.0 added functional options to configfetcher
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed
2025-01-14 19:13:05 -06:00
5c2bfcc940 [WIP] v0.7.0
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed
2025-01-14 09:42:43 -06:00
aee513f786 Update CHANGELOG.md
Some checks failed
ci/woodpecker/push/publish-docs Pipeline was successful
ci/woodpecker/push/go-lint Pipeline failed
2025-01-04 18:29:39 +00:00
6b99cfa196 update CI config
All checks were successful
ci/woodpecker/push/publish-docs Pipeline was successful
2025-01-04 10:30:33 -06:00
c24e8086e9 v0.6.1
All checks were successful
ci/woodpecker/push/publish-docs Pipeline was successful
ci/woodpecker/tag/gitea Pipeline was successful
ci/woodpecker/tag/publish-docs Pipeline was successful
2025-01-04 09:51:44 -06:00
c12bbe3bee v0.6.1 2025-01-04 09:51:19 -06:00
33febd662e v0.6.1 2025-01-04 09:51:08 -06:00
5635c1edd0 v0.6.1 2025-01-04 09:50:45 -06:00
e169778c82 update changelog
All checks were successful
ci/woodpecker/push/publish-docs Pipeline was successful
2025-01-04 01:23:31 -06:00
c838bfe815 pipeline changes
All checks were successful
ci/woodpecker/push/publish-docs Pipeline was successful
2025-01-04 01:18:06 -06:00
e81a5def47 docs changes
Some checks failed
ci/woodpecker/push/publish-docs Pipeline failed
2025-01-04 01:10:39 -06:00
18884c640d docs changes
Some checks failed
ci/woodpecker/push/publish-docs Pipeline failed
2025-01-04 01:03:03 -06:00
ee2256bfb2 docs changes
Some checks failed
ci/woodpecker/push/publish-docs Pipeline failed
2025-01-04 00:39:11 -06:00
82d79c520a docs and pipeline changes
Some checks failed
ci/woodpecker/push/publish-docs Pipeline failed
2025-01-04 00:36:53 -06:00
c30ae2ac3e docs and pipeline changes 2025-01-04 00:35:32 -06:00
fc738597ff docs and pipeline changes 2025-01-04 00:26:53 -06:00
aebef21eb4 docs
All checks were successful
ci/woodpecker/push/publish-docs Pipeline was successful
2025-01-04 00:21:37 -06:00
71 changed files with 2378 additions and 869 deletions

3
.changes/v0.6.1.md Normal file
View File

@ -0,0 +1,3 @@
## v0.6.1 - 2025-01-04
### Fixed
* When running a list, hooks now run explicitly after the command executes. Fixed panic due to improper logic.

16
.changes/v0.7.0.md Normal file
View File

@ -0,0 +1,16 @@
## v0.7.0 - 2025-02-11
### Added
* [feat]: package `packageOperation` option `checkVersion` implemented
* user management added - see docs
* Support for remote config sources. Only config file and list can be used for now.
* Cache functionality - still a WIP
* Flag `--s3-endpoint` for config file fetching from S3
### Changed
* Internal refactoring of config setup
* Formatting and sending for notifications
* name of `configfetcher` to `remotefetcher`
* Flags that took comma-separated lists now have to be passed multiple times for each argument.
* Hosts passed to `exec host` now checked against default SSH config files
### Fixed
* Parsing of remote URLs when determining list config file path
* Incorrect error notification template value

3
.changes/v0.7.1.md Normal file
View File

@ -0,0 +1,3 @@
## v0.7.1 - 2025-02-14
### Fixed
* Incorrect local config file loading logic caused files to not be detected

3
.changes/v0.7.2.md Normal file
View File

@ -0,0 +1,3 @@
## v0.7.2 - 2025-02-14
### Fixed
* CI configs

3
.changes/v0.7.3.md Normal file
View File

@ -0,0 +1,3 @@
## v0.7.3 - 2025-02-14
### Changed
* GoReleaser configs

5
.changes/v0.7.4.md Normal file
View File

@ -0,0 +1,5 @@
## v0.7.4 - 2025-02-14
### Changed
* CI configs
### Fixed
* v0.7.1: Incorrect local config file loading logic caused files to not be detected

5
.changes/v0.7.5.md Normal file
View File

@ -0,0 +1,5 @@
## v0.7.5 - 2025-02-14
### Changed
* CI configs
### Fixed
* v0.7.1: Incorrect local config file loading logic caused files to not be detected

4
.changes/v0.7.6.md Normal file
View File

@ -0,0 +1,4 @@
## v0.7.6 - 2025-02-14
### Fixed
* v0.7.1: Incorrect local config file loading logic caused files to not be detected
* CI configs

4
.changes/v0.7.7.md Normal file
View File

@ -0,0 +1,4 @@
## v0.7.7 - 2025-02-14
### Fixed
* v0.7.1: Incorrect local config file loading logic caused files to not be detected
* CI configs

4
.changes/v0.7.8.md Normal file
View File

@ -0,0 +1,4 @@
## v0.7.8 - 2025-02-14
### Fixed
* Github CI config
* v0.7.1: Incorrect local config file loading logic caused files to not be detected

6
.changes/v0.8.0.md Normal file
View File

@ -0,0 +1,6 @@
## v0.8.0 - 2025-02-15
### Changed
* Breaking: `cmd-lists` key changed to `cmdLists`
* Properly load list config
* Config file loading properly errors
* CI Configs

View File

@ -15,26 +15,26 @@ jobs:
goreleaser: goreleaser:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- run: git fetch --force --tags - run: git fetch --force --tags
- uses: actions/setup-go@v4 - uses: actions/setup-go@v5
with: with:
go-version: '1.20' go-version: '1.23'
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.
- name: Get tag - uses: olegtarasov/get-tag@v2.1.4
id: tag id: tagName
uses: dawidd6/action-get-tag@v1
with: with:
# Optionally strip `v` prefix # tagRegex: "foobar-(.*)" # Optional. Returns specified group text as tag name. Full tag string is returned if regex is not defined.
strip_v: false tagRegexGroup: 1 # Optional. Default is 1.
- uses: goreleaser/goreleaser-action@v4 - uses: goreleaser/goreleaser-action@v6
with: with:
distribution: goreleaser distribution: goreleaser
version: latest version: 2.7.0
args: release --release-notes=".changes/${{steps.tag.outputs.tag}}.md" -f .goreleaser/github.yml --clean args: release --release-notes=".changes/${{ env.GIT_TAG_NAME }}.md" -f .goreleaser/github.yml --clean
env: env:
GITHUB_TOKEN: ${{ secrets.GORELEASER_TOKEN }} GITHUB_TOKEN: ${{ secrets.GORELEASER_TOKEN }}
GIT_TAG_NAME: ${{ steps.tagName.outputs.tag }}

10
.gitignore vendored
View File

@ -1,2 +1,12 @@
dist/ dist/
.codegpt
*.log
*.sh
*.yaml
*.yml
+.changie.yaml
+.changes/

View File

@ -6,7 +6,6 @@ before:
builds: builds:
- env: - env:
- CGO_ENABLED=0 - CGO_ENABLED=0
- GOPROXY=https://goproxy.io
goos: goos:
- freebsd - freebsd
- linux - linux
@ -16,7 +15,7 @@ builds:
- arm64 - arm64
archives: archives:
- format: tar.gz - formats: tar.gz
# this name template makes the OS and Arch compatible with the results of uname. # this name template makes the OS and Arch compatible with the results of uname.
name_template: >- name_template: >-
{{ .ProjectName }}_{{ .Version }}_ {{ .ProjectName }}_{{ .Version }}_
@ -28,7 +27,7 @@ archives:
# use zip for windows archives # use zip for windows archives
format_overrides: format_overrides:
- goos: windows - goos: windows
format: zip formats: [zip]
checksum: checksum:
name_template: 'checksums.txt' name_template: 'checksums.txt'
snapshot: snapshot:

View File

@ -1,4 +1,3 @@
# 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 version: 2
before: before:
@ -17,7 +16,7 @@ builds:
- arm64 - arm64
archives: archives:
- format: tar.gz - formats: tar.gz
# this name template makes the OS and Arch compatible with the results of uname. # this name template makes the OS and Arch compatible with the results of uname.
name_template: >- name_template: >-
{{ .ProjectName }}_{{ .Version }}_ {{ .ProjectName }}_{{ .Version }}_
@ -29,7 +28,7 @@ archives:
# use zip for windows archives # use zip for windows archives
format_overrides: format_overrides:
- goos: windows - goos: windows
format: zip formats: [zip]
checksum: checksum:
name_template: 'checksums.txt' name_template: 'checksums.txt'
snapshot: snapshot:

View File

@ -1,6 +1,7 @@
{ {
"cSpell.words": [ "cSpell.words": [
"Cmds", "Cmds",
"remotefetcher",
"knadh", "knadh",
"koanf", "koanf",
"mattn", "mattn",

View File

@ -1,11 +1,20 @@
name: goreleaser release
steps: steps:
release: golang:
image: goreleaser/goreleaser image: golang:1.23
commands: commands:
- go mod tidy
- go install github.com/goreleaser/goreleaser/v2@v2.7.0
- goreleaser release -f .goreleaser/gitea.yml --release-notes=".changes/$(go run backy.go version -V).md" - goreleaser release -f .goreleaser/gitea.yml --release-notes=".changes/$(go run backy.go version -V).md"
secrets: [ gitea_token ] environment:
GITEA_TOKEN:
from_secret: gitea_token
when: when:
event: tag event: tag
# release:
# image: goreleaser/goreleaser
# commands:
when: when:
- event: tag - event: tag

View File

@ -1,15 +1,12 @@
steps: steps:
build: build:
image: klakegg/hugo:ext-debian-ci image: hugomods/hugo:ci
commands: commands:
- 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 mod get -u ./... - hugo mod get -u github.com/divinerites/plausible-hugo
- hugo mod get -u github.com/McShelby/hugo-theme-relearn@7.3.1
- hugo - hugo
when:
- event: push
branch: master
path: "docs/*"
deploy: deploy:
image: codingkoopa/git-rsync-openssh image: codingkoopa/git-rsync-openssh
@ -26,9 +23,14 @@ steps:
- echo "$SSH_DEPLOY_KEY" | tr -d '\r' | DISPLAY=":0.0" SSH_ASKPASS=~/.ssh/.print_ssh_password setsid ssh-add - - echo "$SSH_DEPLOY_KEY" | tr -d '\r' | DISPLAY=":0.0" SSH_ASKPASS=~/.ssh/.print_ssh_password setsid ssh-add -
- rsync -atv --delete --progress public/ backy@backy.cybershell.xyz:docs - rsync -atv --delete --progress public/ backy@backy.cybershell.xyz:docs
- 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 ] environment:
SSH_HOST_KEY:
from_secret: ssh_host_key
SSH_DEPLOY_KEY:
from_secret: ssh_deploy_key
SSH_PASSPHRASE:
from_secret: ssh_passphrase
when: when:
- event: push - branch: master
branch: master - path: 'docs/**'
path: "docs/*"

View File

@ -6,6 +6,77 @@ 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.8.0 - 2025-02-15
### Changed
* Breaking: `cmd-lists` key changed to `cmdLists`
* Properly load list config
* Config file loading properly errors
* CI Configs
## v0.7.8 - 2025-02-14
### Fixed
* Github CI config
* v0.7.1: Incorrect local config file loading logic caused files to not be detected
## v0.7.7 - 2025-02-14
### Fixed
* v0.7.1: Incorrect local config file loading logic caused files to not be detected
* CI configs
## v0.7.6 - 2025-02-14
### Fixed
* v0.7.1: Incorrect local config file loading logic caused files to not be detected
* CI configs
## v0.7.5 - 2025-02-14
### Changed
* CI configs
### Fixed
* v0.7.1: Incorrect local config file loading logic caused files to not be detected
## v0.7.4 - 2025-02-14
### Changed
* CI configs
### Fixed
* v0.7.1: Incorrect local config file loading logic caused files to not be detected
## v0.7.3 - 2025-02-14
### Changed
* GoReleaser configs
## v0.7.2 - 2025-02-14
### Fixed
* CI configs
## v0.7.1 - 2025-02-14
### Fixed
* Incorrect local config file loading logic caused files to not be detected
## v0.7.0 - 2025-02-11
### Added
* [feat]: package `packageOperation` option `checkVersion` implemented
* user management added - see docs
* Support for remote config sources. Only config file and list can be used for now.
* Cache functionality - still a WIP
* Flag `--s3-endpoint` for config file fetching from S3
### Changed
* Internal refactoring of config setup
* Formatting and sending for notifications
* name of `configfetcher` to `remotefetcher`
* Flags that took comma-separated lists now have to be passed multiple times for each argument.
* Hosts passed to `exec host` now checked against default SSH config files
### Fixed
* Parsing of remote URLs when determining list config file path
* Incorrect error notification template value
## v0.6.1 - 2025-01-04
### Fixed
* When running a list, hooks now run explicitly after the command executes. Fixed panic due to improper logic.
## v0.6.0 - 2025-01-04
### Added
* Command Type Package - allows one to perform package operations [docs](https://backy.cybershell.xyz/config/packages/)
* Exec subcommand `host` allows for parallel execution of commands on hosts. [See docs](https://backy.cybershell.xyz/cli/exec)
## v0.5.0 - 2024-11-19 ## v0.5.0 - 2024-11-19
### Added ### Added
* Lists can now go in a file. See docs for more information. * Lists can now go in a file. See docs for more information.

21
backy.code-workspace Normal file
View File

@ -0,0 +1,21 @@
{
"folders": [
{
"name": "backy",
"path": "."
}
],
"settings": {
"cSpell.words": [
"Cmds",
"remotefetcher",
"knadh",
"koanf",
"mattn",
"maunium",
"mautrix",
"nikoksr",
"Strs"
]
}
}

View File

@ -12,8 +12,8 @@ import (
var ( var (
backupCmd = &cobra.Command{ backupCmd = &cobra.Command{
Use: "backup [--lists=list1,list2,... | -l list1, list2,...]", Use: "backup [--lists=list1 --lists list2 ... | -l list1 -l list2 ...]",
Short: "Runs commands defined in config file.", Short: "Runs commands defined in config file. Use -l flag multiple times to run multiple lists.",
Long: "Backup executes commands defined in config file.\nUse the --lists or -l flag to execute the specified lists. If not flag is not given, all lists will be executed.", Long: "Backup executes commands defined in config file.\nUse the --lists or -l flag to execute the specified lists. If not flag is not given, all lists will be executed.",
Run: Backup, Run: Backup,
} }
@ -23,16 +23,16 @@ var (
var cmdLists []string var cmdLists []string
func init() { func init() {
parseS3Config()
backupCmd.Flags().StringSliceVarP(&cmdLists, "lists", "l", nil, "Accepts comma-separated names of command lists to execute.") backupCmd.Flags().StringArrayVarP(&cmdLists, "lists", "l", nil, "Accepts comma-separated names of command lists to execute.")
} }
func Backup(cmd *cobra.Command, args []string) { func Backup(cmd *cobra.Command, args []string) {
backyConfOpts := backy.NewOpts(cfgFile, backy.AddCommandLists(cmdLists)) backyConfOpts := backy.NewOpts(cfgFile, backy.AddCommandLists(cmdLists))
backyConfOpts.InitConfig() backyConfOpts.InitConfig()
backyConfOpts.ReadConfig()
backy.ReadConfig(backyConfOpts)
backyConfOpts.RunListConfig("") backyConfOpts.RunListConfig("")
for _, host := range backyConfOpts.Hosts { for _, host := range backyConfOpts.Hosts {

View File

@ -16,9 +16,11 @@ var (
) )
func cron(cmd *cobra.Command, args []string) { func cron(cmd *cobra.Command, args []string) {
parseS3Config()
opts := backy.NewOpts(cfgFile, backy.CronEnabled()) opts := backy.NewOpts(cfgFile, backy.EnableCron())
opts.InitConfig() opts.InitConfig()
backy.ReadConfig(opts) opts.ReadConfig()
opts.Cron() opts.Cron()
} }

View File

@ -23,18 +23,17 @@ var (
func init() { func init() {
execCmd.AddCommand(hostExecCommand) execCmd.AddCommand(hostExecCommand)
hostExecCommand.Flags().StringSliceVarP(&hostsList, "hosts", "m", nil, "Accepts comma-separated names of hosts.")
hostExecCommand.Flags().StringSliceVarP(&cmdList, "commands", "c", nil, "Accepts comma-separated names of commands.")
} }
func execute(cmd *cobra.Command, args []string) { func execute(cmd *cobra.Command, args []string) {
parseS3Config()
if len(args) < 1 { if len(args) < 1 {
logging.ExitWithMSG("Please provide a command to run. Pass --help to see options.", 1, nil) logging.ExitWithMSG("Please provide a command to run. Pass --help to see options.", 1, nil)
} }
opts := backy.NewOpts(cfgFile, backy.AddCommands(args)) opts := backy.NewOpts(cfgFile, backy.AddCommands(args), backy.SetLogFile(logFile))
opts.InitConfig() opts.InitConfig()
// opts.InitMongo() opts.ReadConfig()
backy.ReadConfig(opts).ExecuteCmds(opts) opts.ExecuteCmds()
} }

View File

@ -8,19 +8,25 @@ import (
var ( var (
hostExecCommand = &cobra.Command{ hostExecCommand = &cobra.Command{
Use: "host [--commands=command1,command2, ... | -c command1,command2, ...] [--hosts=host1,hosts2, ... | -m host1,host2, ...] ", Use: "host [--command=command1 --command=command2 ... | -c command1 -c command2 ...] [--hosts=host1 --hosts=hosts2 ... | -m host1 -m host2 ...] ",
Short: "Runs command defined in config file on the hosts in order specified.", Short: "Runs command defined in config file on the hosts in order specified.",
Long: "Host executes specified commands on the hosts defined in config file.\nUse the --commands or -c flag to choose the commands.", Long: "Host executes specified commands on the hosts defined in config file.\nUse the --commands or -c flag to choose the commands.",
Run: Host, Run: Host,
} }
) )
// Holds command list to run // Holds list of hosts to run commands on
var hostsList []string var hostsList []string
// Holds command list to run
var cmdList []string var cmdList []string
func init() { func init() {
hostExecCommand.Flags().StringArrayVarP(&hostsList, "hosts", "m", nil, "Accepts space-separated names of hosts. Specify multiple times for multiple hosts.")
hostExecCommand.Flags().StringArrayVarP(&cmdList, "command", "c", nil, "Accepts space-separated names of commands. Specify multiple times for multiple commands.")
parseS3Config()
} }
// cli input should be hosts and commands. Hosts are defined in config files. // cli input should be hosts and commands. Hosts are defined in config files.
@ -29,22 +35,28 @@ func init() {
// 2. stdin (on command line) (TODO) // 2. stdin (on command line) (TODO)
func Host(cmd *cobra.Command, args []string) { func Host(cmd *cobra.Command, args []string) {
backyConfOpts := backy.NewOpts(cfgFile) backyConfOpts := backy.NewOpts(cfgFile, backy.SetLogFile(logFile))
backyConfOpts.InitConfig() backyConfOpts.InitConfig()
backy.ReadConfig(backyConfOpts) backyConfOpts.ReadConfig()
// check CLI input // check CLI input
if hostsList == nil { if hostsList == nil {
logging.ExitWithMSG("error: hosts must be specified", 1, &backyConfOpts.Logger) logging.ExitWithMSG("error: hosts must be specified", 1, &backyConfOpts.Logger)
} }
// host is only checked when we read the SSH File
// so a check may not be needed here
for _, h := range hostsList { for _, h := range hostsList {
// check if h exists in the config file
_, hostFound := backyConfOpts.Hosts[h] _, hostFound := backyConfOpts.Hosts[h]
if !hostFound { if !hostFound {
// check if h exists in the SSH config file
hostFoundInConfig, s := backy.CheckIfHostHasHostName(h)
if !hostFoundInConfig {
logging.ExitWithMSG("host "+h+" not found", 1, &backyConfOpts.Logger) logging.ExitWithMSG("host "+h+" not found", 1, &backyConfOpts.Logger)
} }
// create host with hostname and host
backyConfOpts.Hosts[h] = &backy.Host{Host: h, HostName: s}
}
} }
if cmdList == nil { if cmdList == nil {
logging.ExitWithMSG("error: commands must be specified", 1, &backyConfOpts.Logger) logging.ExitWithMSG("error: commands must be specified", 1, &backyConfOpts.Logger)

View File

@ -31,19 +31,19 @@ func init() {
func List(cmd *cobra.Command, args []string) { func List(cmd *cobra.Command, args []string) {
// settup based on whats passed in: // setup based on whats passed in:
// - cmds // - cmds
// - lists // - lists
// - if none, list all commands // - if none, list all commands
if cmdLists != nil { if cmdLists != nil {
} }
parseS3Config()
opts := backy.NewOpts(cfgFile) opts := backy.NewOpts(cfgFile)
opts.InitConfig() opts.InitConfig()
opts.ReadConfig()
opts = backy.ReadConfig(opts)
opts.ListCommand("rm-sn-db") opts.ListCommand("rm-sn-db")
} }

View File

@ -15,6 +15,8 @@ var (
// Used for flags. // Used for flags.
cfgFile string cfgFile string
verbose bool verbose bool
logFile string
s3Endpoint string
rootCmd = &cobra.Command{ rootCmd = &cobra.Command{
Use: "backy", Use: "backy",
@ -32,9 +34,16 @@ func Execute() {
} }
func init() { func init() {
rootCmd.PersistentFlags().StringVar(&logFile, "log-file", "", "log file to write to")
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.PersistentFlags().StringVar(&s3Endpoint, "s3-endpoint", "", "Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.")
rootCmd.AddCommand(backupCmd, execCmd, cronCmd, versionCmd, listCmd) rootCmd.AddCommand(backupCmd, execCmd, cronCmd, versionCmd, listCmd)
} }
func parseS3Config() {
if s3Endpoint != "" {
os.Setenv("S3_ENDPOINT", s3Endpoint)
}
}

View File

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

View File

@ -1,137 +0,0 @@
baseURL = 'http://backy.cybershell.xyz/'
languageCode = 'en-us'
title = 'A tool for commands'
# Change the default theme to be use when building the site with Hugo
theme = [ "hugo-theme-relearn", "plausible-hugo"] # Add this theme to your already existing other themes
# For search functionality
[outputs]
home = [ "HTML", "RSS", "PRINT"]
[module]
[[module.imports]]
path = "github.com/divinerites/plausible-hugo"
[params]
# This controls whether submenus will be expanded (true), or collapsed (false) in the
# menu; if no setting is given, the first menu level is set to false, all others to true;
# this can be overridden in the pages frontmatter
alwaysopen = false
# Prefix URL to edit current page. Will display an "Edit" button on top right hand corner of every page.
# Useful to give opportunity to people to create merge request for your doc.
# See the config.toml file from this documentation site to have an example.
editURL = ""
# Description of the site, will be used in meta information
description = ""
# Shows a checkmark for visited pages on the menu
showVisitedLinks = false
# Disable search function. It will hide search bar
disableSearch = false
# Disable search in hidden pages, otherwise they will be shown in search box
disableSearchHiddenPages = false
# Disables hidden pages from showing up in the sitemap and on Google (et all), otherwise they may be indexed by search engines
disableSeoHiddenPages = false
# Disables hidden pages from showing up on the tags page although the tag term will be displayed even if all pages are hidden
disableTagHiddenPages = false
# Javascript and CSS cache are automatically busted when new version of site is generated.
# Set this to true to disable this behavior (some proxies don't handle well this optimization)
disableAssetsBusting = false
# Set this to true if you want to disable generation for generator version meta tags of hugo and the theme;
# don't forget to also set Hugo's disableHugoGeneratorInject=true, otherwise it will generate a meta tag into your home page
disableGeneratorVersion = false
# Set this to true to disable copy-to-clipboard button for inline code.
disableInlineCopyToClipBoard = false
# A title for shortcuts in menu is set by default. Set this to true to disable it.
disableShortcutsTitle = false
# If set to false, a Home button will appear below the search bar on the menu.
# It is redirecting to the landing page of the current language if specified. (Default is "/")
disableLandingPageButton = true
# When using mulitlingual website, disable the switch language button.
disableLanguageSwitchingButton = false
# Hide breadcrumbs in the header and only show the current page title
disableBreadcrumb = true
# If set to true, hide table of contents menu in the header of all pages
disableToc = false
# If set to false, load the MathJax module on every page regardless if a MathJax shortcode is present
math = true
# Specifies the remote location of the MathJax js
customMathJaxURL = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"
# Initialization parameter for MathJax, see MathJax documentation
mathJaxInitialize = "{}"
# If set to false, load the Mermaid module on every page regardless if a Mermaid shortcode or Mermaid codefence is present
mermaid = true
# Specifies the remote location of the Mermaid js
customMermaidURL = "https://unpkg.com/mermaid/dist/mermaid.min.js"
# Initialization parameter for Mermaid, see Mermaid documentation
mermaidInitialize = "{ \"theme\": \"default\" }"
# If set to false, load the Swagger module on every page regardless if a Swagger shortcode is present
disableSwagger = false
# Specifies the remote location of the RapiDoc js
customSwaggerURL = "https://unpkg.com/rapidoc/dist/rapidoc-min.js"
# Initialization parameter for Swagger, see RapiDoc documentation
swaggerInitialize = "{ \"theme\": \"light\" }"
# Hide Next and Previous page buttons normally displayed full height beside content
disableNextPrev = true
# Order sections in menu by "weight" or "title". Default to "weight";
# this can be overridden in the pages frontmatter
ordersectionsby = "weight"
[[params.themeVariant]]
auto = []
identifier = 'relearn-auto'
name = 'Relearn Light/Dark'
[[params.themeVariant]]
identifier = 'relearn-light'
[[params.themeVariant]]
identifier = 'relearn-dark'
[[params.themeVariant]]
identifier = 'relearn-bright'
[[params.themeVariant]]
auto = ['zen-light', 'zen-dark']
identifier = 'zen-auto'
name = 'Zen Light/Dark'
[[params.themeVariant]]
identifier = 'zen-light'
[[params.themeVariant]]
identifier = 'zen-dark'
[[params.themeVariant]]
auto = ['learn', 'neon']
identifier = 'retro-auto'
name = 'Retro Learn/Neon'
[[params.themeVariant]]
identifier = 'neon'
[[params.themeVariant]]
identifier = 'learn'
# Change the title separator. Default to "::".
titleSeparator = "-"
# If set to true, the menu in the sidebar will be displayed in a collapsible tree view. Although the functionality works with old browsers (IE11), the display of the expander icons is limited to modern browsers
collapsibleMenu = true
# If a single page can contain content in multiple languages, add those here
additionalContentLanguage = [ "en" ]
# If set to true, no index.html will be appended to prettyURLs; this will cause pages not
# to be servable from the file system
disableExplicitIndexURLs = false
# For external links you can define how they are opened in your browser; this setting will only be applied to the content area but not the shortcut menu
externalLinkTarget = "_blank"
# Author of the site, will be used in meta information
[params.author]
name = "Andrew Woodlee"
[params.plausible]
enable = true # Whether to enable plausible tracking
domain = "backy.cybershell.xyz" # Plausible "domain" name/id in your dashboard
outbound_link = true
gitstar = false
selfhosted_domain = "stats.andrewnw.com" # Self-hosted plausible domain

78
docs/config.yaml Normal file
View File

@ -0,0 +1,78 @@
baseURL: https://backy.cybershell.xyz/
languageCode: en-us
title: A tool for commands
theme:
- hugo-theme-relearn
- plausible-hugo
outputs:
home:
- HTML
- RSS
- PRINT
module:
imports:
- path: github.com/divinerites/plausible-hugo
- path: github.com/McShelby/hugo-theme-relearn
params:
themeVariant:
- auto: []
identifier: relearn-auto
name: Relearn Light/Dark
- identifier: relearn-light
- identifier: relearn-dark
- identifier: relearn-bright
- auto:
- zen-light
- zen-dark
identifier: zen-auto
name: Zen Light/Dark
- identifier: zen-light
- identifier: zen-dark
- auto:
- learn
- neon
identifier: retro-auto
name: Retro Learn/Neon
- identifier: neon
- identifier: learn
plausible:
enable: true
domain: backy.cybershell.xyz
outbound_link: true
gitstar: false
selfhosted_domain: stats.andrewnw.com
author:
name: Andrew Woodlee
alwaysopen: false
editURL: ""
description: ""
showVisitedLinks: false
disableSearch: false
disableSearchHiddenPages: false
disableSeoHiddenPages: false
disableTagHiddenPages: false
disableAssetsBusting: false
disableGeneratorVersion: false
disableInlineCopyToClipBoard: false
disableShortcutsTitle: false
disableLandingPageButton: true
disableLanguageSwitchingButton: false
disableBreadcrumb: true
disableToc: false
math: true
customMathJaxURL: https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js
mathJaxInitialize: "{}"
mermaid: true
customMermaidURL: https://unpkg.com/mermaid/dist/mermaid.min.js
mermaidInitialize: '{ "theme": "default" }'
disableSwagger: false
customSwaggerURL: https://unpkg.com/rapidoc/dist/rapidoc-min.js
swaggerInitialize: '{ "theme": "light" }'
disableNextPrev: true
ordersectionsby: weight
titleSeparator: "-"
collapsibleMenu: true
additionalContentLanguage:
- en
disableExplicitIndexURLs: false
externalLinkTarget: _blank

View File

@ -17,6 +17,10 @@ Feel free to open a [PR](https://git.andrewnw.xyz/CyberShell/backy/pulls), raise
- Allows easy configuration of executable commands - Allows easy configuration of executable commands
- Allows for running package operations
- Allows configuring failure, success, and final hooks
- Allows for commands to be run on many hosts over SSH - Allows for commands to be run on many hosts over SSH
- Commands can be grouped in list to run in specific order - Commands can be grouped in list to run in specific order

View File

@ -14,7 +14,7 @@ Usage:
backy [command] backy [command]
Available Commands: Available Commands:
backup Runs commands defined in config file. backup Runs commands defined in config file. Use -l flag multiple times to run multiple lists.
completion Generate the autocompletion script for the specified shell completion Generate the autocompletion script for the specified shell
cron Starts a scheduler that runs lists defined in config file. cron Starts a scheduler that runs lists defined in config file.
exec Runs commands defined in config file in order given. exec Runs commands defined in config file in order given.
@ -25,6 +25,8 @@ Available Commands:
Flags: Flags:
-f, --config string config file to read from -f, --config string config file to read from
-h, --help help for backy -h, --help help for backy
--log-file string log file to write to
--s3-endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.
-v, --verbose Sets verbose level -v, --verbose Sets verbose level
Use "backy [command] --help" for more information about a command. Use "backy [command] --help" for more information about a command.
@ -39,14 +41,16 @@ Backup executes commands defined in config file.
Use the --lists or -l flag to execute the specified lists. If not flag is not given, all lists will be executed. Use the --lists or -l flag to execute the specified lists. If not flag is not given, all lists will be executed.
Usage: Usage:
backy backup [--lists=list1,list2,... | -l list1, list2,...] [flags] backy backup [--lists=list1 --lists list2 ... | -l list1 -l list2 ...] [flags]
Flags: Flags:
-h, --help help for backup -h, --help help for backup
-l, --lists strings Accepts comma-separated names of command lists to execute. -l, --lists stringArray Accepts comma-separated names of command lists to execute.
Global Flags: Global Flags:
-f, --config string config file to read from -f, --config string config file to read from
--log-file string log file to write to
--s3-endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.
-v, --verbose Sets verbose level -v, --verbose Sets verbose level
``` ```
@ -63,6 +67,8 @@ Flags:
Global Flags: Global Flags:
-f, --config string config file to read from -f, --config string config file to read from
--log-file string log file to write to
--s3-endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.
-v, --verbose Sets verbose level -v, --verbose Sets verbose level
``` ```
@ -83,6 +89,8 @@ Flags:
Global Flags: Global Flags:
-f, --config string config file to read from -f, --config string config file to read from
--log-file string log file to write to
--s3-endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.
-v, --verbose Sets verbose level -v, --verbose Sets verbose level
Use "backy exec [command] --help" for more information about a command. Use "backy exec [command] --help" for more information about a command.
@ -95,15 +103,17 @@ Host executes specified commands on the hosts defined in config file.
Use the --commands or -c flag to choose the commands. Use the --commands or -c flag to choose the commands.
Usage: Usage:
backy exec host [--commands=command1,command2, ... | -c command1,command2, ...] [--hosts=host1,hosts2, ... | -m host1,host2, ...] [flags] backy exec host [--command=command1 --command=command2 ... | -c command1 -c command2 ...] [--hosts=host1 --hosts=hosts2 ... | -m host1 -m host2 ...] [flags]
Flags: Flags:
-c, --commands strings Accepts comma-separated names of commands. -c, --command stringArray Accepts space-separated names of commands. Specify multiple times for multiple commands.
-h, --help help for host -h, --help help for host
-m, --hosts strings Accepts comma-separated names of hosts. -m, --hosts stringArray Accepts space-separated names of hosts. Specify multiple times for multiple hosts.
Global Flags: Global Flags:
-f, --config string config file to read from -f, --config string config file to read from
--log-file string log file to write to
--s3-endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.
-v, --verbose Sets verbose level -v, --verbose Sets verbose level
``` ```
@ -122,6 +132,8 @@ Flags:
Global Flags: Global Flags:
-f, --config string config file to read from -f, --config string config file to read from
--log-file string log file to write to
--s3-endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.
-v, --verbose Sets verbose level -v, --verbose Sets verbose level
``` ```
@ -141,5 +153,7 @@ Flags:
Global Flags: Global Flags:
-f, --config string config file to read from -f, --config string config file to read from
--log-file string log file to write to
--s3-endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.
-v, --verbose Sets verbose level -v, --verbose Sets verbose level
``` ```

View File

@ -2,7 +2,7 @@
title: Exec title: Exec
--- ---
The `exec` subcommand can do somethings that the configuration file can't do yet. The command `exec host` can execute commands on many hosts. The `exec` subcommand can do some things that the configuration file can't do yet. The command `exec host` can execute commands on many hosts.
`exec host` takes the following arguments: `exec host` takes the following arguments:

View File

@ -10,13 +10,31 @@ This is the section on the config file.
To use a specific file: To use a specific file:
```backy [command] -f /path/to/file``` ```backy [command] -f /path/to/file```
You can also use a remote file:
```
backy [command] -f `s3/http source`
```
See remote resources docs for specific info.
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:
1. `./backy.yml` 1. `./backy.yml`
2. `./backy.yaml` 2. `./backy.yaml`
3. `~/.config/backy.yml` 3. The same two files above contained in a `backy` subdirectory under in what is returned by Go's `os` package function `UserConfigDir()`.
4. `~/.config/backy.yaml`
Create a file at `~/.config/backy.yml`. {{% expand title="`UserConfigDir()` documentation:" %}}
See the rest of the documentation in this section to configure it. Up-to date documentation for this function may be found on [GoDoc](https://pkg.go.dev/os#UserConfigDir).
>UserConfigDir returns the default root directory to use for user-specific configuration data. Users should create their own application-specific subdirectory within this one and use that.
>On Unix systems, it returns $XDG_CONFIG_HOME as specified by https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if non-empty, else $HOME/.config. On Darwin, it returns $HOME/Library/Application Support. On Windows, it returns %AppData%. On Plan 9, it returns $home/lib.
>If the location cannot be determined (for example, $HOME is not defined), then it will return an error.
{{% /expand %}}
See the rest of the documentation, titles included below, in this section to configure it.
{{% children description="true" %}}

View File

@ -2,7 +2,7 @@
title: "Command Lists" title: "Command Lists"
weight: 2 weight: 2
description: > description: >
This page tells you how to get started with Backy. This page tells you how to use command lists.
--- ---
Command lists are for executing commands in sequence and getting notifications from them. Command lists are for executing commands in sequence and getting notifications from them.
@ -11,10 +11,18 @@ 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: Lists can go in a separate file. Command lists should be in a separate file if:
1. key 'cmd-lists.file' is found 1. key 'cmdLists.file' is specified
2. hosts.yml or hosts.yaml is found in the same directory as the backy config file 2. lists.yml or lists.yaml is found in the same directory as the backy config file (this includes remote config files as of v0.7.0)
```yaml {{% notice info %}}
The lists file is also checked in remote resources.
The lists file is ignored under the following condition:
If a remote config file is specified (on the command-line using `-f`) and the lists file is not found in the same directory, the lists file is assumed to not exist.
{{% /notice %}}
```yaml {lineNos="true" wrap="true" title="yaml"}
test2: test2:
name: test2 name: test2
order: order:
@ -62,14 +70,14 @@ Name is optional. If name is not defined, name will be the object's map key.
Backy also has a cron mode, so one can run `backy cron` and start a process that schedules jobs to run at times defined in the configuration file. Backy also has a cron mode, so one can run `backy cron` and start a process that schedules jobs to run at times defined in the configuration file.
Adding `cron: 0 0 1 * * *` to a `cmd-lists` object will schedule the list at 1 in the morning. See [https://crontab.guru/](https://crontab.guru/) for reference. Adding `cron: 0 0 1 * * *` to a `cmdLists` object will schedule the list at 1 in the morning. See [https://crontab.guru/](https://crontab.guru/) for reference.
{{% notice tip %}} {{% notice tip %}}
Note: Backy uses the second field of cron, so add anything except * to the beginning of a regular cron expression. Note: Backy uses the second field of cron, so add anything except `*` to the beginning of a regular cron expression.
{{% /notice %}} {{% /notice %}}
```yaml ```yaml {lineNos="true" wrap="true" title="yaml"}
cmd-lists: cmdLists:
  docker-container-backup: # this can be any name you want   docker-container-backup: # this can be any name you want
    # all commands have to be defined     # all commands have to be defined
    order:     order:

View File

@ -1,11 +1,10 @@
--- ---
title: "Commands" title: "Commands"
description: Commands are just that, commands
weight: 1 weight: 1
--- ---
The yaml top-level map can be any string.
The top-level name must be unique.
### Example Config ### Example Config
@ -42,8 +41,8 @@ Values available for this section **(case-sensitive)**:
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `cmd` | Defines the command to execute | `string` | yes | | `cmd` | Defines the command to execute | `string` | yes |
| `Args` | Defines the arguments to the command | `[]string` | no | | `Args` | Defines the arguments to the command | `[]string` | no |
| `environment` | Defines evironment variables for the command | `[]string` | no | | `environment` | Defines environment variables for the command | `[]string` | no |
| `type` | May be `scriptFile`, `script`, or `package`. Runs script from local machine on remote. `Package` is the only one that can be run on local and remote hosts. | `string` | no | | `type` | See documentation further down the page. Additional fields may be required. | `string` | no |
| `getOutput` | Command(s) output is in the notification(s) | `bool` | no | | `getOutput` | Command(s) output is in the notification(s) | `bool` | no |
| `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` or `script`, this file is prepended to the input. | `string` | no | | `scriptEnvFile` | When type is `scriptFile` or `script`, this file is prepended to the input. | `string` | no |
@ -107,13 +106,14 @@ This is useful for specifying environment variables or other things so they don'
### type ### type
May be `scriptFile` or `script`. Runs script from local machine on remote host passed to the SSH session as standard input. The following options are available:
If `type` is `script`, `cmd` is used as the script. | name | description |
| --- | --- |
If `type` is `scriptFile`, cmd must be a script file. | script | `cmd` is used as the script |
| scriptFile | Can only be run on a host. `cmd` is read and used as the script, and `scriptEnvFile` can be used to add env variables |
If `type` is `package`, there are additional fields that must be specified. | package | Run package operations. See [dedicated page](/config/packages) for configuring package commands |
| user | Run user operations. See [dedicated page](/config/user-commands) for configuring package commands |
### environment ### environment
@ -121,7 +121,7 @@ The environment variables support expansion:
- using escaped values `$VAR` or `${VAR}` - using escaped values `$VAR` or `${VAR}`
For now, the variables have to be defined in an `.env` file in the same directory as the config file. For now, the variables have to be defined in an `.env` file in the same directory that the program is run from.
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.

View File

@ -1,6 +1,7 @@
--- ---
title: "Packages" title: "Packages"
weight: 2 weight: 2
description: This is dedicated to package commands.
--- ---
This is dedicated to `package` commands. The command `type` field must be `package`. Package is a type that allows one to perform package operations. There are several additional options available when `type` is `package`: This is dedicated to `package` commands. The command `type` field must be `package`. Package is a type that allows one to perform package operations. There are several additional options available when `type` is `package`:
@ -9,8 +10,8 @@ This is dedicated to `package` commands. The command `type` field must be `packa
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `packageName` | The name of a package to be modified. | `string` | yes | | `packageName` | The name of a package to be modified. | `string` | yes |
| `packageManager` | The name of the package manger to be used. | `string` | yes | | `packageManager` | The name of the package manger to be used. | `string` | yes |
| `packageOperation` | The type of operation to be perform. | `string` | yes | | `packageOperation` | The type of operation to perform. | `string` | yes |
| `packageVersion` | The version of a package to be modified. | `string` | no | | `packageVersion` | The version of a package. | `string` | no |
#### example #### example
@ -34,6 +35,7 @@ The following package operations are supported:
- `install` - `install`
- `remove` - `remove`
- `upgrade` - `upgrade`
- `checkVersion`
#### packageManager #### packageManager
@ -45,11 +47,11 @@ The following package managers are recognized:
#### package command args #### package command args
You can add additional arguments using the standard `Args` key. This is useful for adding more packages. You can add additional arguments using the standard `Args` key. This is useful for adding more packages, yet it does not work with `checkVersion`.
### Development ### Development
The PackageManager interface provides an easy to enforce functions and options. There are two interfaces, `PackageManager` and `ConfigurablePackageManager` in the directory `pkg/pkgman`. Go's import-cycle "feature" caused me to implement functional options using a third interface. `PackageManagerOption`is a function that takes an interface. The PackageManager interface provides an easy way to enforce functions and options. There are two interfaces, `PackageManager` and `ConfigurablePackageManager` in the directory `pkg/pkgman`. Go's import-cycle "feature" caused me to implement functional options using a third interface. `PackageManagerOption`is a function that takes an interface.
#### PackageManager #### PackageManager

View File

@ -0,0 +1,62 @@
---
title: "User commands"
weight: 2
description: This is dedicated to user commands.
---
This is dedicated to `user` commands. The command `type` field must be `user`. User is a type that allows one to perform user operations. There are several additional options available when `type` is `user`:
| name | notes | type | required |
| --- | --- | --- | --- |
| `userName` | The name of a user to be configured. | `string` | yes |
| `userOperation` | The type of operation to perform. | `string` | yes |
| `userID` | The user ID to use. | `string` | yes |
| `userGroups` | The groups the user should be added to. | `[]string` | yes |
| `userShell` | The shell for the user. | `string` | yes |
| `userHome` | The user's home directory. | `string` | no |
#### example
The following is an example of a package command:
```yaml
addUser:
name: add user backy with custom home dir
type: user
userName: backy
userHome: /opt/backy
userOperation: add
host: some-host
```
#### userOperation
The following package operations are supported:
- `add`
- `remove`
- `modify`
- `password`
- `checkIfExists`
### Development
The UserManager interface provides an way easy to add new commands. There is one interface `Usermanager` in directory `pkg/usermanager`.
#### UserManager
```go
// UserManager defines the interface for user management operations.
// All functions but one return a string for the command and any args.
type UserManager interface {
AddUser(username, homeDir, shell string, isSystem bool, groups, args []string) (string, []string)
RemoveUser(username string) (string, []string)
ModifyUser(username, homeDir, shell string, groups []string) (string, []string)
// Modify password uses chpasswd for Linux systems to build the command to change the password
// Should return a password as the last argument
// TODO: refactor when adding more systems instead of Linux
ModifyPassword(username, password string) (string, *strings.Reader, string)
UserExists(username string) (string, []string)
}
```

View File

@ -0,0 +1,24 @@
---
title: "Hosts"
weight: 2
description: >
This page tells you how to use hosts.
---
| Key | Description | Type | Required |
|----------------------|---------------------------------------------------------------|----------|----------|
| `OS` | Operating system of the host (used for package commands) | `string` | no |
| `config` | Path to the SSH config file | `string` | no |
| `host` | Specifies the `Host` ssh_config(5) directive | `string` | yes |
| `hostname` | Hostname of the host | `string` | no |
| `knownhostsfile` | Path to the known hosts file | `string` | no |
| `port` | Port number to connect to | `uint16` | no |
| `proxyjump` | Proxy jump hosts, comma-separated | `string` | no |
| `password` | Password for SSH authentication | `string` | no |
| `privatekeypath` | Path to the private key file | `string` | no |
| `privatekeypassword` | Password for the private key file | `string` | no |
| `user` | Username for SSH authentication | `string` | no |
## exec host subcommand
Backy has a subcommand `exec host`. This subcommand takes the flags of `-m host1 -m host2`. For now these hosts need to be defined in the config file.

View File

@ -5,6 +5,7 @@ description: >
This page tells you how to get set up Backy notifications. This page tells you how to get set up Backy notifications.
--- ---
Notifications are only configurable for command lists, as of right now.
Notifications can be sent on command list completion and failure. Notifications can be sent on command list completion and failure.

View File

@ -0,0 +1,17 @@
---
title: "Remote resources"
weight: 2
description: This is dedicated to configuring remote resources.
---
Remote resources can be used for a lot of things, including config files and scripts.
## Config file
For the main config file to be fetched remotely, pass the URL using `-f [url]`.
If using S3, you should use the s3 protocol URI: `s3://bucketName/key/path`. You will also need to set the env variable `S3_ENDPOINT` to the appropriate value. The flag `--s3-endpoint` can be used to override this value or to set this value, if not already set.
## Scripts
Scripts will be coming later.

View File

@ -1,6 +1,7 @@
--- ---
title: "Vault" title: "Vault"
weight: 4 weight: 4
description: Set up and configure vault.
--- ---
[Vault](https://www.vaultproject.io/) is a tool for storing secrets and other data securely. [Vault](https://www.vaultproject.io/) is a tool for storing secrets and other data securely.

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-lists: cmdLists:
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-lists` 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 `cmdLists` key `notifications`.
```yaml ```yaml
notifications: notifications:

View File

@ -3,6 +3,6 @@ module git.andrewnw.xyz/CyberShell/backy/docs
go 1.19 go 1.19
require ( require (
github.com/McShelby/hugo-theme-relearn v0.0.0-20230209073138-890d12ea922d // indirect github.com/McShelby/hugo-theme-relearn v0.0.0-20250103114405-80e448e5bdaa // indirect
github.com/divinerites/plausible-hugo v1.21.1 // indirect github.com/divinerites/plausible-hugo v1.21.1 // indirect
) )

View File

@ -1,4 +1,10 @@
github.com/McShelby/hugo-theme-relearn v0.0.0-20230209073138-890d12ea922d h1:weq1mrQ/qNAvGrNgvZVL1K8adbT3bswZf2ABLr/LCIA= github.com/McShelby/hugo-theme-relearn v0.0.0-20230209073138-890d12ea922d h1:weq1mrQ/qNAvGrNgvZVL1K8adbT3bswZf2ABLr/LCIA=
github.com/McShelby/hugo-theme-relearn v0.0.0-20230209073138-890d12ea922d/go.mod h1:mKQQdxZNIlLvAj8X3tMq+RzntIJSr9z7XdzuMomt0IM= github.com/McShelby/hugo-theme-relearn v0.0.0-20230209073138-890d12ea922d/go.mod h1:mKQQdxZNIlLvAj8X3tMq+RzntIJSr9z7XdzuMomt0IM=
github.com/McShelby/hugo-theme-relearn v0.0.0-20241210183303-16d4de84becf h1:bMx4kwM7Q+dAzvSOWs3XWZ25o+n4mI0GPHqzbzeWb3M=
github.com/McShelby/hugo-theme-relearn v0.0.0-20241210183303-16d4de84becf/go.mod h1:mKQQdxZNIlLvAj8X3tMq+RzntIJSr9z7XdzuMomt0IM=
github.com/McShelby/hugo-theme-relearn v0.0.0-20250102210630-dd0597ffa4b2 h1:sWaC1/dL65v3iRvblEAaBLpKC5TIT0R9JASk1hZNET8=
github.com/McShelby/hugo-theme-relearn v0.0.0-20250102210630-dd0597ffa4b2/go.mod h1:mKQQdxZNIlLvAj8X3tMq+RzntIJSr9z7XdzuMomt0IM=
github.com/McShelby/hugo-theme-relearn v0.0.0-20250103114405-80e448e5bdaa h1:G+OnMEzK4XOzbbcf1SmaGyOYJ0h5idp/IJdguWs8ioU=
github.com/McShelby/hugo-theme-relearn v0.0.0-20250103114405-80e448e5bdaa/go.mod h1:mKQQdxZNIlLvAj8X3tMq+RzntIJSr9z7XdzuMomt0IM=
github.com/divinerites/plausible-hugo v1.21.1 h1:ZTWwjhZ0PmLMacCVGlcGiYFEZW7VaYE767tchDskOug= github.com/divinerites/plausible-hugo v1.21.1 h1:ZTWwjhZ0PmLMacCVGlcGiYFEZW7VaYE767tchDskOug=
github.com/divinerites/plausible-hugo v1.21.1/go.mod h1:cxr+YB3FUwbLon8KCs4pV4Ankbkq6lJxTQUpNb5KqPo= github.com/divinerites/plausible-hugo v1.21.1/go.mod h1:cxr+YB3FUwbLon8KCs4pV4Ankbkq6lJxTQUpNb5KqPo=

View File

@ -28,9 +28,23 @@ commands:
cmd: hostname cmd: hostname
update-docker: update-docker:
type: package type: package
packageManager: apt shell: zsh # best to run package commands in a shell
packageName: docker-ce packageName: docker-ce
packageVersion: "5:27.4.1-1~debian.12~bookworm" Args:
- docker-ce-cli
packageManager: apt
packageOperation: install
update-dockerApt:
# type: package
shell: zsh
cmd: apt
Args:
- update
- "&&"
- apt install -y docker-ce
- docker-ce-cli
packageManager: apt
packageOperation: install
cmd-lists: cmd-lists:
cmds-to-run: # this can be any name you want cmds-to-run: # this can be any name you want
@ -67,7 +81,7 @@ hosts:
# optional # optional
logging: logging:
verbose: true verbose: true
file: /path/to/logs/commands.log file: ./backy.log
console: false console: false
cmd-std-out: false cmd-std-out: false

90
go.mod
View File

@ -1,69 +1,91 @@
module git.andrewnw.xyz/CyberShell/backy module git.andrewnw.xyz/CyberShell/backy
go 1.20 go 1.23
toolchain go1.23.6
replace git.andrewnw.xyz/CyberShell/backy => /home/andrew/Projects/backy replace git.andrewnw.xyz/CyberShell/backy => /home/andrew/Projects/backy
require ( require (
github.com/go-co-op/gocron v1.33.1 github.com/aws/aws-sdk-go-v2/service/s3 v1.76.0
github.com/hashicorp/vault/api v1.10.0 github.com/go-co-op/gocron v1.37.0
github.com/hashicorp/vault/api v1.15.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/kevinburke/ssh_config v1.2.0 github.com/kevinburke/ssh_config v1.2.0
github.com/knadh/koanf/parsers/yaml v0.1.0 github.com/knadh/koanf/parsers/yaml v0.1.0
github.com/knadh/koanf/providers/file v0.1.0 github.com/knadh/koanf/providers/rawbytes v0.1.0
github.com/knadh/koanf/v2 v2.0.1 github.com/knadh/koanf/v2 v2.1.2
github.com/mattn/go-isatty v0.0.19 github.com/mattn/go-isatty v0.0.20
github.com/nikoksr/notify v0.41.0 github.com/minio/minio-go/v7 v7.0.84
github.com/mitchellh/go-homedir v1.1.0
github.com/nikoksr/notify v1.3.0
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.33.0
github.com/spf13/cobra v1.7.0 github.com/sethvargo/go-password v0.3.1
golang.org/x/crypto v0.13.0 github.com/spf13/cobra v1.8.1
golang.org/x/crypto v0.33.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
maunium.net/go/mautrix v0.16.0 gopkg.in/yaml.v3 v3.0.1
mvdan.cc/sh/v3 v3.7.0 maunium.net/go/mautrix v0.23.0
mvdan.cc/sh/v3 v3.10.0
) )
require ( require (
github.com/cenkalti/backoff/v3 v3.2.2 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.36.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.32 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.32 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.32 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.13 // indirect
github.com/aws/smithy-go v1.22.2 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-jose/go-jose/v3 v3.0.0 // indirect github.com/go-ini/ini v1.67.0 // indirect
github.com/google/uuid v1.3.1 // indirect github.com/go-jose/go-jose/v4 v4.0.1 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/google/uuid v1.6.0 // 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
github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.4 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 // indirect github.com/hashicorp/go-secure-stdlib/parseutil v0.1.9 // indirect
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
github.com/hashicorp/go-sockaddr v1.0.5 // indirect github.com/hashicorp/go-sockaddr v1.0.7 // indirect
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.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/knadh/koanf/maps v0.1.1 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/minio/md5-simd v1.1.2 // 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/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/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/rs/xid v1.6.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.6 // indirect
github.com/stretchr/objx v0.5.1 // indirect github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.8.4 // indirect github.com/stretchr/testify v1.10.0 // indirect
github.com/tidwall/gjson v1.16.0 // indirect github.com/tidwall/gjson v1.18.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
go.mau.fi/util v0.0.0-20230906155759-14bad39a8718 // indirect go.mau.fi/util v0.8.4 // 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-20250207012021-f9890c6ad9f3 // indirect
golang.org/x/net v0.15.0 // indirect golang.org/x/net v0.34.0 // indirect
golang.org/x/sync v0.3.0 // indirect golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.12.0 // indirect golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.13.0 // indirect golang.org/x/text v0.22.0 // indirect
golang.org/x/time v0.3.0 // indirect golang.org/x/time v0.10.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
maunium.net/go/maulogger/v2 v2.4.1 // indirect
) )

215
go.sum
View File

@ -1,54 +1,82 @@
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= github.com/aws/aws-sdk-go-v2 v1.36.1 h1:iTDl5U6oAhkNPba0e1t1hrwAo02ZMqbrGq4k5JBWM5E=
github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/aws/aws-sdk-go-v2 v1.36.1/go.mod h1:5PMILGVKiW32oDzjj6RU52yrNrDPUHcbZQYr1sM7qmM=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 h1:zAxi9p3wsZMIaVCdoiQp2uZ9k1LsZvmAnoTBeZPXom0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8/go.mod h1:3XkePX5dSaxveLAYY7nsbsZZrKxCyEuE5pM4ziFxyGg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.32 h1:BjUcr3X3K0wZPGFg2bxOWW3VPN8rkE3/61zhP+IHviA=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.32/go.mod h1:80+OGC/bgzzFFTUmcuwD0lb4YutwQeKLFpmt6hoWapU=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.32 h1:m1GeXHVMJsRsUAqG6HjZWx9dj7F5TR+cF1bjyfYyBd4=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.32/go.mod h1:IitoQxGfaKdVLNg0hD8/DXmAqNy0H4K2H2Sf91ti8sI=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.32 h1:OIHj/nAhVzIXGzbAE+4XmZ8FPvro3THr6NlqErJc3wY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.32/go.mod h1:LiBEsDo34OJXqdDlRGsilhlIiXR7DL+6Cx2f4p1EgzI=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 h1:D4oz8/CzT9bAEYtVhSBmFj2dNOtaHOtMKc2vHBwYizA=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2/go.mod h1:Za3IHqTQ+yNcRHxu1OFucBh0ACZT4j4VQFF0BqpZcLY=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.6 h1:cCBJaT7EeEojpJ4s7wTDbhZlHVJOgNHN7iw6qVurGaw=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.6/go.mod h1:WYH1ABybY7JK9TITPnk6ZlP7gQB8psI4c9qDmMsnLSA=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.13 h1:SYVGSFQHlchIcy6e7x12bsrxClCXSP5et8cqVhL8cuw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.13/go.mod h1:kizuDaLX37bG5WZaoxGPQR/LNFXpxp0vsUnqfkWXfNE=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.13 h1:OBsrtam3rk8NfBEq7OLOMm5HtQ9Yyw32X4UQMya/wjw=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.13/go.mod h1:3U4gFA5pmoCOja7aq4nSaIAGbaOHv2Yl2ug018cmC+Q=
github.com/aws/aws-sdk-go-v2/service/s3 v1.76.0 h1:ehvUZNVrGA1Usa6yYo8A8pUqrigRelWXSbcCqYpRLeI=
github.com/aws/aws-sdk-go-v2/service/s3 v1.76.0/go.mod h1:KuLNrwYJFaC2AVZ+CVVc12k9NyqwgWsoNNHjwqF6QNk=
github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0=
github.com/go-co-op/gocron v1.33.1 h1:wjX+Dg6Ae29a/f9BSQjY1Rl+jflTpW9aDyMqseCj78c= github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY=
github.com/go-co-op/gocron v1.33.1/go.mod h1:NLi+bkm4rRSy1F8U7iacZOz0xPseMoIOnvabGoSe/no= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
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/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 h1:UpiO20jno/eV1eVZcxqWnUohyKRe1g8FPV/xH1s/2qs= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.9 h1:FW0YttEnUNDJ2WL9XcrrfteS1xW8u+sh4ggM8pN5isQ=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.9/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=
github.com/hashicorp/go-sockaddr v1.0.5 h1:dvk7TIXCZpmfOlM+9mlcrWmWjw/wlKT+VDq2wMvfPJU= github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=
github.com/hashicorp/go-sockaddr v1.0.5/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3lyjsj6+CCykpaxI=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/vault/api v1.10.0 h1:/US7sIjWN6Imp4o/Rj1Ce2Nr5bki/AXi9vAW3p2tOJQ= github.com/hashicorp/vault/api v1.15.0 h1:O24FYQCWwhwKnF7CuSqP30S51rTV7vz1iACXE/pj5DA=
github.com/hashicorp/vault/api v1.10.0/go.mod h1:jo5Y/ET+hNyz+JnKDt8XLAdKs+AM0G5W0Vp1IrFI8N8= github.com/hashicorp/vault/api v1.15.0/go.mod h1:+5YTO09JGn0u+b6ySD/LLVf8WkJCPLAL2Vkmrn2+CM8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
@ -57,83 +85,89 @@ 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.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
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=
github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY= github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY=
github.com/knadh/koanf/providers/file v0.1.0 h1:fs6U7nrV58d3CFAFh8VTde8TM262ObYf3ODrc//Lp+c= github.com/knadh/koanf/providers/rawbytes v0.1.0 h1:dpzgu2KO6uf6oCb4aP05KDmKmAmI51k5pe8RYKQ0qME=
github.com/knadh/koanf/providers/file v0.1.0/go.mod h1:rjJ/nHQl64iYCtAW2QQnF0eSmDEX/YZ/eNFj5yR6BvA= github.com/knadh/koanf/providers/rawbytes v0.1.0/go.mod h1:mMTB1/IcJ/yE++A2iEZbY1MLygX7vttU+C+S/YmPu9c=
github.com/knadh/koanf/v2 v2.0.1 h1:1dYGITt1I23x8cfx8ZnldtezdyaZtfAuRtIFOiRzK7g= github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ=
github.com/knadh/koanf/v2 v2.0.1/go.mod h1:ZeiIlIDXTE7w1lMT6UVcNiRAS2/rCeLn/GdLNvY1Dus= github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.84 h1:D1HVmAF8JF8Bpi6IU4V9vIEj+8pc+xU88EWMs2yed0E=
github.com/minio/minio-go/v7 v7.0.84/go.mod h1:57YXpvc5l3rjPdhqNrDsvVlY0qPI6UTk1bflAe+9doY=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
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/nikoksr/notify v0.41.0 h1:4LGE41GpWdHX5M3Xo6DlWRwS2WLDbOq1Rk7IzY4vjmQ= github.com/nikoksr/notify v1.3.0 h1:UxzfxzAYGQD9a5JYLBTVx0lFMxeHCke3rPCkfWdPgLs=
github.com/nikoksr/notify v0.41.0/go.mod h1:FoE0UVPeopz1Vy5nm9vQZ+JVmYjEIjQgbFstbkw+cRE= github.com/nikoksr/notify v1.3.0/go.mod h1:Xor2hMmkvrCfkCKvXGbcrESez4brac2zQjhd6U2BbeM=
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=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97 h1:3RPlVWzZ/PDqmVuf/FKHARG5EMid/tl7cv54Sw/QRVY= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/sethvargo/go-password v0.3.1 h1:WqrLTjo7X6AcVYfC6R7GtSyuUQR9hGyAj/f1PYQZCJU=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5OptoxrMBcrXzvs=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.16.0 h1:SyXa+dsSPpUlcwEDuKuEBJEz5vzTvOea+9rjyYodQFg= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.16.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
@ -141,39 +175,30 @@ 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=
go.mau.fi/util v0.0.0-20230906155759-14bad39a8718 h1:hmm5bZqE0M8+Uvys0HJPCSbAIZIwYtTkBKYPjAWHuMM= go.mau.fi/util v0.8.4 h1:mVKlJcXWfVo8ZW3f4vqtjGpqtZqJvX4ETekxawt2vnQ=
go.mau.fi/util v0.0.0-20230906155759-14bad39a8718/go.mod h1:AxuJUMCxpzgJ5eV9JbPWKRH8aAJJidxetNdUj7qcb84= go.mau.fi/util v0.8.4/go.mod h1:MOfGTs1CBuK6ERTcSL4lb5YU7/ujz09eOPVEDckuazY=
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.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3 h1:qNgPs5exUA+G0C96DrPwNrvLSj7GT/9D+3WMWUcUg34=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
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/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-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.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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
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=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@ -184,9 +209,7 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8= maunium.net/go/mautrix v0.23.0 h1:HNlR19eew5lvrNSL2muhExaGhYdaGk5FfEiA82QqUP4=
maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho= maunium.net/go/mautrix v0.23.0/go.mod h1:AGnnaz3ylGikUo1I1MJVn9QLsl2No1/ZNnGDyO0QD5s=
maunium.net/go/mautrix v0.16.0 h1:iUqCzJE2yqBC1ddAK6eAn159My8rLb4X8g4SFtQh2Dk= mvdan.cc/sh/v3 v3.10.0 h1:v9z7N1DLZ7owyLM/SXZQkBSXcwr2IGMm2LY2pmhVXj4=
maunium.net/go/mautrix v0.16.0/go.mod h1:XAjE9pTSGcr6vXaiNgQGiip7tddJ8FQV1a29u2QdBG4= mvdan.cc/sh/v3 v3.10.0/go.mod h1:z/mSSVyLFGZzqb3ZIKojjyqIx/xbmz/UHdCSv9HmqXY=
mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg=
mvdan.cc/sh/v3 v3.7.0/go.mod h1:K2gwkaesF/D7av7Kxl0HbF5kGOd2ArupNTX3X44+8l8=

View File

@ -34,24 +34,34 @@ var Sprintf = fmt.Sprintf
func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([]string, error) { func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([]string, error) {
var ( var (
outputArr []string ArgsStr string // concatenating the arguments
ArgsStr string
cmdOutBuf bytes.Buffer cmdOutBuf bytes.Buffer
cmdOutWriters io.Writer cmdOutWriters io.Writer
errSSH error
envVars = environmentVars{ envVars = environmentVars{
file: command.Env, file: command.Env,
env: command.Environment, env: command.Environment,
} }
outputArr []string // holds the output strings returned by processes
) )
// Get the command type
// This must be done before concatenating the arguments
command = getCommandType(command)
for _, v := range command.Args { for _, v := range command.Args {
ArgsStr += fmt.Sprintf(" %s", v) ArgsStr += fmt.Sprintf(" %s", v)
} }
command = getPackageCommand(command) // print the user's password if it is updated
if command.Type == "user" {
if command.UserOperation == "password" {
cmdCtxLogger.Info().Str("password", command.UserPassword).Msg("user password to be updated")
}
}
var errSSH error
// is host defined // is host defined
if command.Host != nil { if command.Host != nil {
outputArr, errSSH = command.RunCmdSSH(cmdCtxLogger, opts) outputArr, errSSH = command.RunCmdSSH(cmdCtxLogger, opts)
@ -60,52 +70,45 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
} }
} else { } else {
// Handle package operations
if command.Type == "package" && command.PackageOperation == "checkVersion" {
cmdCtxLogger.Info().Str("package", command.PackageName).Msg("Checking package versions")
// Execute the package version command
cmd := exec.Command(command.Cmd, command.Args...)
cmdOutWriters = io.MultiWriter(&cmdOutBuf)
cmd.Stdout = cmdOutWriters
cmd.Stderr = cmdOutWriters
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("error running command %s: %w", ArgsStr, err)
}
return parsePackageVersion(cmdOutBuf.String(), cmdCtxLogger, command, cmdOutBuf)
}
var localCMD *exec.Cmd
var err error var err error
if command.Shell != "" { if command.Shell != "" {
cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s on local machine in %s", command.Name, 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)
localCMD := exec.Command(command.Shell, "-c", ArgsStr) localCMD = exec.Command(command.Shell, "-c", ArgsStr)
if command.Dir != nil { } else {
localCMD.Dir = *command.Dir
}
injectEnvIntoLocalCMD(envVars, localCMD, cmdCtxLogger)
cmdOutWriters = io.MultiWriter(&cmdOutBuf)
if IsCmdStdOutEnabled() {
cmdOutWriters = io.MultiWriter(os.Stdout, &cmdOutBuf)
}
localCMD.Stdout = cmdOutWriters
localCMD.Stderr = cmdOutWriters
err = localCMD.Run()
outScanner := bufio.NewScanner(&cmdOutBuf)
for outScanner.Scan() {
outMap := make(map[string]interface{})
outMap["cmd"] = command.Name
outMap["output"] = outScanner.Text()
if str, ok := outMap["output"].(string); ok {
outputArr = append(outputArr, str)
}
cmdCtxLogger.Info().Fields(outMap).Send()
}
if err != nil {
cmdCtxLogger.Error().Err(fmt.Errorf("error when running cmd %s: %w", command.Name, err)).Send()
return outputArr, err
}
return outputArr, nil
}
cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s on local machine", command.Name)).Send() cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s on local machine", command.Name)).Send()
localCMD := exec.Command(command.Cmd, command.Args...) // execute package commands in a shell
if command.Type == "package" {
cmdCtxLogger.Info().Str("package", command.PackageName).Msg("Executing package command")
ArgsStr = fmt.Sprintf("%s %s", command.Cmd, ArgsStr)
localCMD = exec.Command("/bin/sh", "-c", ArgsStr)
} else {
localCMD = exec.Command(command.Cmd, command.Args...)
}
}
if command.Dir != nil { if command.Dir != nil {
localCMD.Dir = *command.Dir localCMD.Dir = *command.Dir
@ -134,7 +137,9 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
if str, ok := outMap["output"].(string); ok { if str, ok := outMap["output"].(string); ok {
outputArr = append(outputArr, str) outputArr = append(outputArr, str)
} }
// if command.GetOutput {
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.Name, err)).Send() cmdCtxLogger.Error().Err(fmt.Errorf("error when running cmd %s: %w", command.Name, err)).Send()
@ -144,115 +149,124 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
return outputArr, nil return outputArr, nil
} }
// cmdListWorker func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<- CmdResult, 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 := map[string]interface{}{"list": list.Name}
fieldsMap["list"] = list.Name
var cmdLogger zerolog.Logger var cmdLogger zerolog.Logger
var cmdsRan []string
var count int // count of how many commands have been executed var outStructArr []outStruct
var cmdsRan []string // store the commands that have been executed var hasError bool // Tracks if any command in the list failed
var outStructArr []outStruct // stores output messages
for _, cmd := range list.Order { for _, cmd := range list.Order {
currentCmd := opts.Cmds[cmd].Name
fieldsMap["cmd"] = opts.Cmds[cmd].Name
cmdToRun := opts.Cmds[cmd] cmdToRun := opts.Cmds[cmd]
currentCmd := cmdToRun.Name
fieldsMap["cmd"] = currentCmd
cmdLogger = cmdToRun.GenerateLogger(opts) cmdLogger = cmdToRun.GenerateLogger(opts)
cmdLogger.Info().Fields(fieldsMap).Send() cmdLogger.Info().Fields(fieldsMap).Send()
outputArr, runOutErr := cmdToRun.RunCmd(cmdLogger, opts) outputArr, runErr := cmdToRun.RunCmd(cmdLogger, opts)
if list.NotifyConfig != nil {
// check if the command output should be included
if cmdToRun.GetOutput || list.GetOutput {
outputStruct := outStruct{
CmdName: cmdToRun.Name,
CmdExecuted: currentCmd,
Output: outputArr,
}
outStructArr = append(outStructArr, outputStruct)
}
}
count++
if runOutErr != nil {
res.ErrCmd = cmd
if list.NotifyConfig != nil {
var errMsg bytes.Buffer
errStruct := make(map[string]interface{})
errStruct["listName"] = list.Name
errStruct["Command"] = currentCmd
errStruct["Cmd"] = cmd
errStruct["Args"] = opts.Cmds[cmd].Args
errStruct["Err"] = runOutErr
errStruct["CmdsRan"] = cmdsRan
errStruct["Output"] = outputArr
errStruct["CmdOutput"] = outStructArr
tmpErr := msgTemps.err.Execute(&errMsg, errStruct)
if tmpErr != nil {
cmdLogger.Err(tmpErr).Send()
}
notifySendErr := list.NotifyConfig.Send(context.Background(), fmt.Sprintf("List %s failed", list.Name), errMsg.String())
if notifySendErr != nil {
cmdLogger.Err(notifySendErr).Send()
}
}
cmdLogger.Err(runOutErr).Send()
break
} else {
cmdsRan = append(cmdsRan, cmd) cmdsRan = append(cmdsRan, cmd)
if count == len(list.Order) { if runErr != nil {
var successMsg bytes.Buffer
// if notification config is not nil, and NotifyOnSuccess is true or GetOuput is true, // Log the error and send a failed result
// then send notification cmdLogger.Err(runErr).Send()
if list.NotifyConfig != nil && (list.NotifyOnSuccess || list.GetOutput) { results <- CmdResult{CmdName: cmd, ListName: list.Name, Error: runErr}
successStruct := make(map[string]interface{})
successStruct["listName"] = list.Name // Execute error hooks for the failed command
successStruct["CmdsRan"] = cmdsRan cmdToRun.ExecuteHooks("error", opts)
successStruct["CmdOutput"] = outStructArr // Notify failure
if list.NotifyConfig != nil {
tmpErr := msgTemps.success.Execute(&successMsg, successStruct) notifyError(cmdLogger, msgTemps, list, cmdsRan, outStructArr, runErr, cmdToRun)
}
if tmpErr != nil { hasError = true
cmdLogger.Err(tmpErr).Send()
break break
} }
err := list.NotifyConfig.Send(context.Background(), fmt.Sprintf("List %s succeeded", list.Name), successMsg.String()) // Collect output if required
if list.GetOutput || cmdToRun.GetOutput {
if err != nil { outStructArr = append(outStructArr, outStruct{
cmdLogger.Err(err).Send() CmdName: currentCmd,
} CmdExecuted: currentCmd,
} Output: outputArr,
} })
} }
} }
results <- res.ErrCmd // Notify success if no errors occurred
if !hasError && list.NotifyConfig != nil && (list.NotifyOnSuccess || list.GetOutput) {
notifySuccess(cmdLogger, msgTemps, list, cmdsRan, outStructArr)
} }
// Execute success and final hooks for all commands
for _, cmd := range list.Order {
cmdToRun := opts.Cmds[cmd]
// Execute success hooks if the command succeeded
if !hasError || cmdsRanContains(cmd, cmdsRan) {
cmdToRun.ExecuteHooks("success", opts)
}
// Execute final hooks for every command
cmdToRun.ExecuteHooks("final", opts)
}
// Send the final result for the list
if hasError {
results <- CmdResult{CmdName: cmdsRan[len(cmdsRan)-1], ListName: list.Name, Error: fmt.Errorf("list execution failed")}
} else {
results <- CmdResult{CmdName: cmdsRan[len(cmdsRan)-1], ListName: list.Name, Error: nil}
}
}
}
// Helper to check if a command is in the list of executed commands
func cmdsRanContains(cmd string, cmdsRan []string) bool {
for _, c := range cmdsRan {
if c == cmd {
return true
}
}
return false
}
// Helper to notify errors
func notifyError(logger zerolog.Logger, templates *msgTemplates, list *CmdList, cmdsRan []string, outStructArr []outStruct, err error, cmd *Command) {
errStruct := map[string]interface{}{
"listName": list.Name,
"CmdsRan": cmdsRan,
"CmdOutput": outStructArr,
"Err": err,
"CmdName": cmd.Name,
"Command": cmd.Cmd,
"Args": cmd.Args,
}
var errMsg bytes.Buffer
if e := templates.err.Execute(&errMsg, errStruct); e != nil {
logger.Err(e).Send()
return
}
if e := list.NotifyConfig.Send(context.Background(), fmt.Sprintf("List %s failed", list.Name), errMsg.String()); e != nil {
logger.Err(e).Send()
}
}
// Helper to notify success
func notifySuccess(logger zerolog.Logger, templates *msgTemplates, list *CmdList, cmdsRan []string, outStructArr []outStruct) {
successStruct := map[string]interface{}{
"listName": list.Name,
"CmdsRan": cmdsRan,
"CmdOutput": outStructArr,
}
var successMsg bytes.Buffer
if e := templates.success.Execute(&successMsg, successStruct); e != nil {
logger.Err(e).Send()
return
}
if e := list.NotifyConfig.Send(context.Background(), fmt.Sprintf("List %s succeeded", list.Name), successMsg.String()); e != nil {
logger.Err(e).Send()
}
} }
// RunListConfig runs a command list from the ConfigFile. // RunListConfig runs a command list from the ConfigFile.
@ -263,52 +277,35 @@ func (opts *ConfigOpts) RunListConfig(cron string) {
} }
configListsLen := len(opts.CmdConfigLists) configListsLen := len(opts.CmdConfigLists)
listChan := make(chan *CmdList, configListsLen) listChan := make(chan *CmdList, configListsLen)
results := make(chan string) results := make(chan CmdResult, configListsLen)
// This starts up list workers, initially blocked // Start workers
// because there are no jobs yet.
for w := 1; w <= configListsLen; w++ { for w := 1; w <= configListsLen; w++ {
go cmdListWorker(mTemps, listChan, results, opts) go cmdListWorker(mTemps, listChan, results, opts)
} }
// Enqueue jobs
for listName, cmdConfig := range opts.CmdConfigLists { for listName, cmdConfig := range opts.CmdConfigLists {
if cmdConfig.Name == "" { if cmdConfig.Name == "" {
cmdConfig.Name = listName cmdConfig.Name = listName
} }
if cron != "" { if cron == "" || cron == cmdConfig.Cron {
if cron == cmdConfig.Cron {
listChan <- cmdConfig
}
} else {
listChan <- cmdConfig listChan <- cmdConfig
} }
} }
close(listChan) close(listChan)
// Process results
for a := 1; a <= configListsLen; a++ { for a := 1; a <= configListsLen; a++ {
l := <-results result := <-results
opts.Logger.Debug().Msgf("Processing result for list %s, command %s", result.ListName, result.CmdName)
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)
// Process final hooks for the list (already handled in worker)
} }
// execute final hooks
opts.Cmds[l].ExecuteHooks("final", opts)
}
opts.closeHostConnections() opts.closeHostConnections()
} }
func (config *ConfigOpts) ExecuteCmds(opts *ConfigOpts) { func (opts *ConfigOpts) ExecuteCmds() {
for _, cmd := range opts.executeCmds { for _, cmd := range opts.executeCmds {
cmdToRun := opts.Cmds[cmd] cmdToRun := opts.Cmds[cmd]
cmdLogger := cmdToRun.GenerateLogger(opts) cmdLogger := cmdToRun.GenerateLogger(opts)
@ -428,3 +425,14 @@ func (opts *ConfigOpts) ExecCmdsSSH(cmdList []string, hostsList []string) {
} }
} }
} }
// func executeUserCommands() []string {
// }
// // parseRemoteSources parses source and validates fields using sourceType
// func (c *Command) parseRemoteSources(source, sourceType string) {
// switch sourceType {
// }
// }

View File

@ -4,86 +4,133 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"net/url"
"os" "os"
"path" "path"
"runtime"
"strings" "strings"
"git.andrewnw.xyz/CyberShell/backy/pkg/logging" "git.andrewnw.xyz/CyberShell/backy/pkg/logging"
"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman" "git.andrewnw.xyz/CyberShell/backy/pkg/pkgman"
"git.andrewnw.xyz/CyberShell/backy/pkg/remotefetcher"
"git.andrewnw.xyz/CyberShell/backy/pkg/usermanager"
vault "github.com/hashicorp/vault/api" vault "github.com/hashicorp/vault/api"
"github.com/knadh/koanf/parsers/yaml" "github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/file" "github.com/knadh/koanf/providers/rawbytes"
"github.com/knadh/koanf/v2" "github.com/knadh/koanf/v2"
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
"github.com/rs/zerolog" "github.com/rs/zerolog"
) )
var homeDir string
var homeDirErr error
var backyHomeConfDir string
var configFiles []string
const macroStart string = "%{" const macroStart string = "%{"
const macroEnd string = "}%" const macroEnd string = "}%"
const envMacroStart string = "%{env:" const envMacroStart string = "%{env:"
const vaultMacroStart string = "%{env:" const vaultMacroStart string = "%{vault:"
func (opts *ConfigOpts) InitConfig() { func (opts *ConfigOpts) InitConfig() {
var err error
homeDir, homeDirErr = os.UserHomeDir() homeConfigDir, err := os.UserConfigDir()
if err != nil {
if homeDirErr != nil { logging.ExitWithMSG(err.Error(), 1, nil)
fmt.Println(homeDirErr) }
logging.ExitWithMSG(homeDirErr.Error(), 1, nil) homeCacheDir, err := os.UserCacheDir()
if err != nil {
logging.ExitWithMSG(err.Error(), 1, nil)
} }
backyHomeConfDir = homeDir + "/.config/backy/" backyHomeConfDir := path.Join(homeConfigDir, "backy")
configFiles := []string{
configFiles = []string{"./backy.yml", "./backy.yaml", backyHomeConfDir + "backy.yml", backyHomeConfDir + "backy.yaml"} "./backy.yml", "./backy.yaml",
path.Join(backyHomeConfDir, "backy.yml"),
path.Join(backyHomeConfDir, "backy.yaml"),
}
backyKoanf := koanf.New(".") backyKoanf := koanf.New(".")
opts.ConfigFilePath = strings.TrimSpace(opts.ConfigFilePath) opts.ConfigFilePath = strings.TrimSpace(opts.ConfigFilePath)
if opts.ConfigFilePath != "" { // metadataFile := "hashMetadataSample.yml"
err := testFile(opts.ConfigFilePath)
cacheDir := homeCacheDir
// Load metadata from file
opts.CachedData, err = remotefetcher.LoadMetadataFromFile(path.Join(backyHomeConfDir, "cache.yml"))
if err != nil { if err != nil {
logging.ExitWithMSG(fmt.Sprintf("Could not open config file %s: %v", opts.ConfigFilePath, err), 1, nil) fmt.Println("Error loading metadata:", err)
logging.ExitWithMSG(err.Error(), 1, &opts.Logger)
} }
if err := backyKoanf.Load(file.Provider(opts.ConfigFilePath), yaml.Parser()); err != nil { // Initialize cache with loaded metadata
logging.ExitWithMSG(fmt.Sprintf("error loading config: %v", err), 1, &opts.Logger) cache, err := remotefetcher.NewCache(path.Join(backyHomeConfDir, "cache.yml"), cacheDir)
if err != nil {
fmt.Println("Error initializing cache:", err)
logging.ExitWithMSG(err.Error(), 1, &opts.Logger)
} }
// Populate cache with loaded metadata
for _, data := range opts.CachedData {
if err := cache.AddDataToStore(data.Hash, *data); err != nil {
logging.ExitWithMSG(err.Error(), 1, &opts.Logger)
}
}
opts.Cache, err = remotefetcher.NewCache(path.Join(backyHomeConfDir, "cache.yml"), backyHomeConfDir)
if err != nil {
logging.ExitWithMSG(fmt.Sprintf("error initializing cache: %v", err), 1, nil)
}
// Initialize the fetcher
// println("Creating new fetcher for source", opts.ConfigFilePath)
fetcher, err := remotefetcher.NewRemoteFetcher(opts.ConfigFilePath, opts.Cache)
// println("Created new fetcher for source", opts.ConfigFilePath)
if err != nil {
logging.ExitWithMSG(fmt.Sprintf("error initializing config fetcher: %v", err), 1, nil)
}
if opts.ConfigFilePath != "" {
loadConfigFile(fetcher, opts.ConfigFilePath, backyKoanf, opts)
} else { } else {
loadDefaultConfigFiles(fetcher, configFiles, backyKoanf, opts)
cFileFailures := 0
for _, c := range configFiles {
if err := backyKoanf.Load(file.Provider(c), yaml.Parser()); err != nil {
cFileFailures++
} else {
opts.ConfigFilePath = c
break
} }
}
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)
}
}
opts.koanf = backyKoanf opts.koanf = backyKoanf
} }
// ReadConfig validates and reads the config file. func loadConfigFile(fetcher remotefetcher.RemoteFetcher, filePath string, k *koanf.Koanf, opts *ConfigOpts) {
func ReadConfig(opts *ConfigOpts) *ConfigOpts { data, err := fetcher.Fetch(filePath)
if err != nil {
if isatty.IsTerminal(os.Stdout.Fd()) { logging.ExitWithMSG(fmt.Sprintf("Could not fetch config file %s: %v", filePath, err), 1, nil)
os.Setenv("BACKY_TERM", "enabled")
} else if isatty.IsCygwinTerminal(os.Stdout.Fd()) {
os.Setenv("BACKY_TERM", "enabled")
} else {
os.Setenv("BACKY_TERM", "disabled")
} }
if err := k.Load(rawbytes.Provider(data), yaml.Parser()); err != nil {
logging.ExitWithMSG(fmt.Sprintf("error loading config: %v", err), 1, &opts.Logger)
}
}
func loadDefaultConfigFiles(fetcher remotefetcher.RemoteFetcher, configFiles []string, k *koanf.Koanf, opts *ConfigOpts) {
cFileFailures := 0
for _, c := range configFiles {
opts.ConfigFilePath = c
data, err := fetcher.Fetch(c)
if err != nil {
cFileFailures++
continue
}
if data != nil {
if err := k.Load(rawbytes.Provider(data), yaml.Parser()); err == nil {
break
} else {
logging.ExitWithMSG(fmt.Sprintf("error loading config from file %s: %v", c, err), 1, &opts.Logger)
}
}
}
if cFileFailures == len(configFiles) {
logging.ExitWithMSG("Could not find any valid local config file", 1, nil)
}
}
func (opts *ConfigOpts) ReadConfig() *ConfigOpts {
setTerminalEnv()
backyKoanf := opts.koanf backyKoanf := opts.koanf
opts.loadEnv() opts.loadEnv()
@ -94,228 +141,39 @@ func ReadConfig(opts *ConfigOpts) *ConfigOpts {
CheckConfigValues(backyKoanf, opts.ConfigFilePath) CheckConfigValues(backyKoanf, opts.ConfigFilePath)
// check for commands in file validateCommands(backyKoanf, opts)
for _, c := range opts.executeCmds {
if !backyKoanf.Exists(getCmdFromConfig(c)) {
logging.ExitWithMSG(Sprintf("command %s is not in config file %s", c, opts.ConfigFilePath), 1, nil)
}
}
// TODO: refactor this further down the line setLoggingOptions(backyKoanf, opts)
// 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 (
isLoggingVerbose bool
logFile string
)
isLoggingVerbose = backyKoanf.Bool(getLoggingKeyFromConfig("verbose"))
logFile = fmt.Sprintf("%s/backy.log", path.Dir(opts.ConfigFilePath)) // get full path to logfile
if backyKoanf.Exists(getLoggingKeyFromConfig("file")) {
logFile = backyKoanf.String(getLoggingKeyFromConfig("file"))
}
zerolog.SetGlobalLevel(zerolog.InfoLevel)
if isLoggingVerbose {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
globalLvl := zerolog.GlobalLevel()
os.Setenv("BACKY_LOGLEVEL", Sprintf("%v", globalLvl))
}
consoleLoggingDisabled := backyKoanf.Bool(getLoggingKeyFromConfig("console-disabled"))
os.Setenv("BACKY_CONSOLE_LOGGING", "enabled")
// Other qualifiers can go here as well
if consoleLoggingDisabled {
os.Setenv("BACKY_CONSOLE_LOGGING", "")
}
writers := logging.SetLoggingWriters(logFile)
log := zerolog.New(writers).With().Timestamp().Logger()
log := setupLogger(opts)
opts.Logger = log opts.Logger = log
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"}) unmarshalConfig(backyKoanf, "commands", &opts.Cmds, opts.Logger)
if unmarshalErr != nil { validateCommandEnvironments(opts)
panic(fmt.Errorf("error unmarshaling cmds struct: %w", unmarshalErr)) unmarshalConfig(backyKoanf, "hosts", &opts.Hosts, opts.Logger)
} resolveHostConfigs(opts)
for cmdName, cmdConf := range opts.Cmds { loadCommandLists(opts, backyKoanf)
envFileErr := testFile(cmdConf.Env)
if envFileErr != nil {
opts.Logger.Info().Str("cmd", cmdName).Err(envFileErr).Send()
os.Exit(1)
}
expandEnvVars(opts.backyEnv, cmdConf.Environment) validateCommandLists(opts)
}
// Get host configurations from config file if opts.cronEnabled && len(opts.CmdConfigLists) == 0 {
unmarshalErr = backyKoanf.UnmarshalWithConf("hosts", &opts.Hosts, koanf.UnmarshalConf{Tag: "yaml"})
if unmarshalErr != nil {
panic(fmt.Errorf("error unmarshalling hosts struct: %w", unmarshalErr))
}
for hostConfigName, host := range opts.Hosts {
if host.Host == "" {
host.Host = hostConfigName
}
if host.ProxyJump != "" {
proxyHosts := strings.Split(host.ProxyJump, ",")
for hostNum, h := range proxyHosts {
if hostNum > 1 {
proxyHost, defined := opts.Hosts[h]
if defined {
host.ProxyHost = append(host.ProxyHost, proxyHost)
} else {
newProxy := &Host{Host: h}
host.ProxyHost = append(host.ProxyHost, newProxy)
}
} else {
proxyHost, defined := opts.Hosts[h]
if defined {
host.ProxyHost = append(host.ProxyHost, proxyHost)
} else {
newHost := &Host{Host: h}
host.ProxyHost = append(host.ProxyHost, newHost)
}
}
}
}
}
// get command lists
// command lists should still be in the same file if no:
// 1. key 'cmd-lists.file' is found
// 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
for cmdListName, cmdList := range opts.CmdConfigLists {
if opts.cronEnabled {
cron := strings.TrimSpace(cmdList.Cron)
if cron == "" {
delete(opts.CmdConfigLists, cmdListName)
}
}
for _, cmdInList := range cmdList.Order {
_, cmdNameFound := opts.Cmds[cmdInList]
if !cmdNameFound {
cmdNotFoundStr := fmt.Sprintf("command %s in list %s is not defined in commands section in config file", cmdInList, cmdListName)
cmdNotFoundErr := errors.New(cmdNotFoundStr)
cmdNotFoundSliceErr = append(cmdNotFoundSliceErr, cmdNotFoundErr)
}
}
}
// Exit program if command is not found from list
if len(cmdNotFoundSliceErr) > 0 {
var cmdNotFoundErrorLog = log.Fatal()
cmdNotFoundErrorLog.Errs("commands not found", cmdNotFoundSliceErr).Send()
}
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)
} }
// process commands
if err := processCmds(opts); err != nil { if err := processCmds(opts); err != nil {
logging.ExitWithMSG(err.Error(), 1, &opts.Logger) logging.ExitWithMSG(err.Error(), 1, &opts.Logger)
} }
if len(opts.executeLists) > 0 { filterExecuteLists(opts)
for l := range opts.CmdConfigLists {
if !contains(opts.executeLists, l) {
delete(opts.CmdConfigLists, l)
}
}
}
if backyKoanf.Exists("notifications") { if backyKoanf.Exists("notifications") {
unmarshalConfig(backyKoanf, "notifications", &opts.NotificationConf, opts.Logger)
unmarshalErr = backyKoanf.UnmarshalWithConf("notifications", &opts.NotificationConf, koanf.UnmarshalConf{Tag: "yaml"})
if unmarshalErr != nil {
fmt.Printf("error unmarshalling notifications object: %v", unmarshalErr)
}
} }
opts.SetupNotify() opts.SetupNotify()
@ -327,6 +185,220 @@ func ReadConfig(opts *ConfigOpts) *ConfigOpts {
return opts return opts
} }
func setTerminalEnv() {
if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) {
os.Setenv("BACKY_TERM", "enabled")
} else {
os.Setenv("BACKY_TERM", "disabled")
}
}
func validateCommands(k *koanf.Koanf, opts *ConfigOpts) {
for _, c := range opts.executeCmds {
if !k.Exists(getCmdFromConfig(c)) {
logging.ExitWithMSG(fmt.Sprintf("command %s is not in config file %s", c, opts.ConfigFilePath), 1, nil)
}
}
}
func setLoggingOptions(k *koanf.Koanf, opts *ConfigOpts) {
isLoggingVerbose := k.Bool(getLoggingKeyFromConfig("verbose"))
// if log file is set in config file and not set on command line, use "./backy.log"
logFile := "./backy.log"
if opts.LogFilePath == "" && k.Exists(getLoggingKeyFromConfig("file")) {
logFile = k.String(getLoggingKeyFromConfig("file"))
opts.LogFilePath = logFile
}
zerolog.SetGlobalLevel(zerolog.InfoLevel)
if isLoggingVerbose {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
os.Setenv("BACKY_LOGLEVEL", fmt.Sprintf("%v", zerolog.GlobalLevel()))
}
if k.Bool(getLoggingKeyFromConfig("console-disabled")) {
os.Setenv("BACKY_CONSOLE_LOGGING", "")
} else {
os.Setenv("BACKY_CONSOLE_LOGGING", "enabled")
}
}
func setupLogger(opts *ConfigOpts) zerolog.Logger {
writers := logging.SetLoggingWriters(opts.LogFilePath)
return zerolog.New(writers).With().Timestamp().Logger()
}
func unmarshalConfig(k *koanf.Koanf, key string, target interface{}, log zerolog.Logger) {
if err := k.UnmarshalWithConf(key, target, koanf.UnmarshalConf{Tag: "yaml"}); err != nil {
logging.ExitWithMSG(fmt.Sprintf("error unmarshalling key %s into struct: %v", key, err), 1, &log)
}
}
func validateCommandEnvironments(opts *ConfigOpts) {
for cmdName, cmdConf := range opts.Cmds {
if err := testFile(cmdConf.Env); err != nil {
opts.Logger.Info().Str("cmd", cmdName).Err(err).Send()
os.Exit(1)
}
expandEnvVars(opts.backyEnv, cmdConf.Environment)
}
}
func resolveHostConfigs(opts *ConfigOpts) {
for hostConfigName, host := range opts.Hosts {
if host.Host == "" {
host.Host = hostConfigName
}
if host.ProxyJump != "" {
resolveProxyHosts(host, opts)
}
}
}
func resolveProxyHosts(host *Host, opts *ConfigOpts) {
proxyHosts := strings.Split(host.ProxyJump, ",")
for _, h := range proxyHosts {
proxyHost, defined := opts.Hosts[h]
if !defined {
proxyHost = &Host{Host: h}
opts.Hosts[h] = proxyHost
}
host.ProxyHost = append(host.ProxyHost, proxyHost)
}
}
func loadCommandLists(opts *ConfigOpts, backyKoanf *koanf.Koanf) {
var backyConfigFileDir string
var listConfigFiles []string
var u *url.URL
// if config file is remote, use the directory of the remote file
if isRemoteURL(opts.ConfigFilePath) {
_, u = getRemoteDir(opts.ConfigFilePath)
// // Still use local list files if a remote config file is used, but use them last
listConfigFiles = []string{u.JoinPath("lists.yml").String(), u.JoinPath("lists.yaml").String()}
} else {
backyConfigFileDir = path.Dir(opts.ConfigFilePath)
// println("backyConfigFileDir", backyConfigFileDir)
listConfigFiles = []string{
// "./lists.yml", "./lists.yaml",
path.Join(backyConfigFileDir, "lists.yml"),
path.Join(backyConfigFileDir, "lists.yaml"),
}
}
listsConfig := koanf.New(".")
for _, l := range listConfigFiles {
if loadListConfigFile(l, listsConfig, opts) {
break
}
}
if backyKoanf.Exists("cmdLists") {
unmarshalConfig(backyKoanf, "cmdLists", &opts.CmdConfigLists, opts.Logger)
if backyKoanf.Exists("cmdLists.file") {
loadCmdListsFile(backyKoanf, listsConfig, opts)
}
}
}
func isRemoteURL(filePath string) bool {
return strings.HasPrefix(filePath, "http://") || strings.HasPrefix(filePath, "https://") || strings.HasPrefix(filePath, "s3://")
}
func getRemoteDir(filePath string) (string, *url.URL) {
u, err := url.Parse(filePath)
if err != nil {
return "", nil
}
// u.Path is the path to the file, stripped of scheme and hostname
u.Path = path.Dir(u.Path)
return u.String(), u
}
func loadListConfigFile(filePath string, k *koanf.Koanf, opts *ConfigOpts) bool {
fetcher, err := remotefetcher.NewRemoteFetcher(filePath, opts.Cache, remotefetcher.IgnoreFileNotFound())
if err != nil {
// if file not found, ignore
if errors.Is(err, remotefetcher.ErrIgnoreFileNotFound) {
println("File not found", filePath)
return true
}
logging.ExitWithMSG(fmt.Sprintf("error initializing config fetcher: %v", err), 1, nil)
}
data, err := fetcher.Fetch(filePath)
if err != nil {
return false
}
if err := k.Load(rawbytes.Provider(data), yaml.Parser()); err != nil {
return false
}
unmarshalConfig(k, "cmdLists", &opts.CmdConfigLists, opts.Logger)
opts.CmdListFile = filePath
return true
}
func loadCmdListsFile(backyKoanf *koanf.Koanf, listsConfig *koanf.Koanf, opts *ConfigOpts) {
opts.CmdListFile = strings.TrimSpace(backyKoanf.String("cmdLists.file"))
if !path.IsAbs(opts.CmdListFile) {
opts.CmdListFile = path.Join(path.Dir(opts.ConfigFilePath), opts.CmdListFile)
}
fetcher, err := remotefetcher.NewRemoteFetcher(opts.CmdListFile, opts.Cache)
if err != nil {
logging.ExitWithMSG(fmt.Sprintf("error initializing config fetcher: %v", err), 1, nil)
}
data, err := fetcher.Fetch(opts.CmdListFile)
if err != nil {
logging.ExitWithMSG(fmt.Sprintf("Could not fetch config file %s: %v", opts.CmdListFile, err), 1, nil)
}
if err := listsConfig.Load(rawbytes.Provider(data), yaml.Parser()); err != nil {
logging.ExitWithMSG(fmt.Sprintf("error loading config: %v", err), 1, &opts.Logger)
}
unmarshalConfig(listsConfig, "cmdLists", &opts.CmdConfigLists, opts.Logger)
opts.Logger.Info().Str("using lists config file", opts.CmdListFile).Send()
}
func validateCommandLists(opts *ConfigOpts) {
var cmdNotFoundSliceErr []error
for cmdListName, cmdList := range opts.CmdConfigLists {
// if cron is enabled and cron is not set, delete the list
if opts.cronEnabled && strings.TrimSpace(cmdList.Cron) == "" {
opts.Logger.Debug().Str("cron", "enabled").Str("list", cmdListName).Msg("cron not set, deleting list")
delete(opts.CmdConfigLists, cmdListName)
continue
}
for _, cmdInList := range cmdList.Order {
if _, cmdNameFound := opts.Cmds[cmdInList]; !cmdNameFound {
cmdNotFoundSliceErr = append(cmdNotFoundSliceErr, fmt.Errorf("command %s in list %s is not defined in commands section in config file", cmdInList, cmdListName))
}
}
}
if len(cmdNotFoundSliceErr) > 0 {
opts.Logger.Fatal().Errs("commands not found", cmdNotFoundSliceErr).Send()
}
}
func filterExecuteLists(opts *ConfigOpts) {
if len(opts.executeLists) > 0 {
for l := range opts.CmdConfigLists {
if !contains(opts.executeLists, l) {
delete(opts.CmdConfigLists, l)
}
}
}
}
func getNestedConfig(nestedConfig, key string) string { func getNestedConfig(nestedConfig, key string) string {
return fmt.Sprintf("%s.%s", nestedConfig, key) return fmt.Sprintf("%s.%s", nestedConfig, key)
} }
@ -342,9 +414,9 @@ func getLoggingKeyFromConfig(key string) string {
return fmt.Sprintf("logging.%s", key) return fmt.Sprintf("logging.%s", key)
} }
func getCmdListFromConfig(list string) string { // func getCmdListFromConfig(list string) string {
return fmt.Sprintf("cmd-lists.%s", list) // return fmt.Sprintf("cmdLists.%s", list)
} // }
func (opts *ConfigOpts) setupVault() error { func (opts *ConfigOpts) setupVault() error {
if !opts.koanf.Bool("vault.enabled") { if !opts.koanf.Bool("vault.enabled") {
@ -448,6 +520,7 @@ func GetVaultKey(str string, opts *ConfigOpts, log zerolog.Logger) string {
} }
func processCmds(opts *ConfigOpts) error { func processCmds(opts *ConfigOpts) error {
// process commands // process commands
for cmdName, cmd := range opts.Cmds { for cmdName, cmd := range opts.Cmds {
@ -503,7 +576,7 @@ func processCmds(opts *ConfigOpts) error {
// Validate the operation // Validate the operation
switch cmd.PackageOperation { switch cmd.PackageOperation {
case "install", "remove", "upgrade": case "install", "remove", "upgrade", "checkVersion":
cmd.pkgMan, err = pkgman.PackageManagerFactory(cmd.PackageManager, pkgman.WithoutAuth()) cmd.pkgMan, err = pkgman.PackageManagerFactory(cmd.PackageManager, pkgman.WithoutAuth())
if err != nil { if err != nil {
return err return err
@ -511,6 +584,40 @@ func processCmds(opts *ConfigOpts) error {
default: default:
return fmt.Errorf("unsupported package operation %s for command %s", cmd.PackageOperation, cmd.Name) return fmt.Errorf("unsupported package operation %s for command %s", cmd.PackageOperation, cmd.Name)
} }
}
// Parse user commands
if cmd.Type == "user" {
if cmd.Username == "" {
return fmt.Errorf("username is required for user command %s", cmd.Name)
}
detectOSType(cmd, opts)
var err error
// Validate the operation
switch cmd.UserOperation {
case "add", "remove", "modify", "checkIfExists", "delete", "password":
cmd.userMan, err = usermanager.NewUserManager(cmd.OS)
if cmd.Host != nil {
host, ok := opts.Hosts[*cmd.Host]
if ok {
cmd.userMan, err = usermanager.NewUserManager(host.OS)
}
}
if err != nil {
return err
}
default:
return fmt.Errorf("unsupported user operation %s for command %s", cmd.UserOperation, cmd.Name)
}
}
if cmd.Type == "remoteScript" {
if !isRemoteURL(cmd.Cmd) {
return fmt.Errorf("remoteScript command %s must be a remote resource", cmdName)
}
} }
} }
@ -519,17 +626,9 @@ func processCmds(opts *ConfigOpts) error {
// processHooks evaluates if hooks are valid Commands // processHooks evaluates if hooks are valid Commands
// //
// Takes the following arguments: // The cmd.hookRefs[hookType] is created with any hooks found.
// //
// 1. a []string of hooks // Returns an error, if any, if the hook command is not found
// 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 { func processHooks(cmd *Command, hooks []string, opts *ConfigOpts, hookType string) error {
// initialize hook type // initialize hook type
@ -557,3 +656,32 @@ func processHooks(cmd *Command, hooks []string, opts *ConfigOpts, hookType strin
} }
return nil return nil
} }
func detectOSType(cmd *Command, opts *ConfigOpts) error {
if cmd.Host == nil {
if runtime.GOOS == "linux" { // also can be specified to FreeBSD
cmd.OS = "linux"
opts.Logger.Info().Msg("Unix/Linux type OS detected")
}
}
host, ok := opts.Hosts[*cmd.Host]
if ok {
if host.OS != "" {
return nil
}
os, err := host.DetectOS(opts)
os = strings.TrimSpace(os)
if err != nil {
return err
}
if os == "" {
return fmt.Errorf("error detecting os for command %s: empty string", cmd.Name)
}
if strings.Contains(os, "linux") {
os = "linux"
}
host.OS = os
}
return nil
}

View File

@ -17,10 +17,7 @@ func (opts *ConfigOpts) Cron() {
s := gocron.NewScheduler(time.Local) s := gocron.NewScheduler(time.Local)
s.TagsUnique() s.TagsUnique()
cmdLists := opts.CmdConfigLists cmdLists := opts.CmdConfigLists
for listName, config := range cmdLists { for _, config := range cmdLists {
if config.Name == "" {
config.Name = listName
}
cron := strings.TrimSpace(config.Cron) cron := strings.TrimSpace(config.Cron)
if cron != "" { if cron != "" {

View File

@ -119,7 +119,7 @@ func (remoteConfig *Host) ConnectToHost(opts *ConfigOpts) error {
return 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
opts.Logger.Info().Str("user", remoteConfig.ClientConfig.User).Send() // opts.Logger.Info().Str("user", remoteConfig.ClientConfig.User).Send()
remoteConfig.SshClient, connectErr = remoteConfig.ConnectThroughBastion(opts.Logger) remoteConfig.SshClient, connectErr = remoteConfig.ConnectThroughBastion(opts.Logger)
if connectErr != nil { if connectErr != nil {
@ -492,9 +492,10 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts)
env: command.Environment, env: command.Environment,
} }
) )
// Get the command type
// This must be done before concatenating the arguments
command.Type = strings.TrimSpace(command.Type) command.Type = strings.TrimSpace(command.Type)
command = getPackageCommand(command) command = getCommandType(command)
// Prepare command arguments // Prepare command arguments
for _, v := range command.Args { for _, v := range command.Args {
@ -506,7 +507,7 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts)
Str("Host", *command.Host). Str("Host", *command.Host).
Msgf("Running %s on host %s", getCommandTypeLabel(command.Type), *command.Host) Msgf("Running %s on host %s", getCommandTypeLabel(command.Type), *command.Host)
cmdCtxLogger.Debug().Str("cmd", command.Cmd).Strs("args", command.Args).Send() // cmdCtxLogger.Debug().Str("cmd", command.Cmd).Strs("args", command.Args).Send()
// Ensure SSH client is connected // Ensure SSH client is connected
if command.RemoteHost.SshClient == nil { if command.RemoteHost.SshClient == nil {
@ -516,7 +517,7 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts)
} }
// Create new SSH session // Create new SSH session
commandSession, err := command.createSSHSession(opts) commandSession, err := command.RemoteHost.createSSHSession(opts)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create SSH session: %w", err) return nil, fmt.Errorf("failed to create SSH session: %w", err)
} }
@ -539,6 +540,27 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts)
return command.runScript(commandSession, cmdCtxLogger, &cmdOutBuf) return command.runScript(commandSession, cmdCtxLogger, &cmdOutBuf)
case "scriptFile": case "scriptFile":
return command.runScriptFile(commandSession, cmdCtxLogger, &cmdOutBuf) return command.runScriptFile(commandSession, cmdCtxLogger, &cmdOutBuf)
case "package":
if command.PackageOperation == "checkVersion" {
commandSession.Stderr = nil
// Execute the package version command remotely
// Parse the output of package version command
// Compare versions
// Check if a specific version is specified
commandSession.Stdout = nil
return checkPackageVersion(cmdCtxLogger, command, commandSession, cmdOutBuf)
} else {
if command.Shell != "" {
ArgsStr = fmt.Sprintf("%s -c '%s %s'", command.Shell, command.Cmd, ArgsStr)
} else {
ArgsStr = fmt.Sprintf("%s %s", command.Cmd, ArgsStr)
}
cmdCtxLogger.Debug().Str("cmd + args", ArgsStr).Send()
// Run simple command
if err := commandSession.Run(ArgsStr); err != nil {
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, true), fmt.Errorf("error running command: %w", err)
}
}
default: default:
if command.Shell != "" { if command.Shell != "" {
ArgsStr = fmt.Sprintf("%s -c '%s %s'", command.Shell, command.Cmd, ArgsStr) ArgsStr = fmt.Sprintf("%s -c '%s %s'", command.Shell, command.Cmd, ArgsStr)
@ -548,11 +570,35 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts)
cmdCtxLogger.Debug().Str("cmd + args", ArgsStr).Send() cmdCtxLogger.Debug().Str("cmd + args", ArgsStr).Send()
// Run simple command // Run simple command
if err := commandSession.Run(ArgsStr); err != nil { if err := commandSession.Run(ArgsStr); err != nil {
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger), fmt.Errorf("error running command: %w", err) return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.GetOutput), fmt.Errorf("error running command: %w", err)
} }
} }
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger), nil return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, true), nil
}
func checkPackageVersion(cmdCtxLogger zerolog.Logger, command *Command, commandSession *ssh.Session, cmdOutBuf bytes.Buffer) ([]string, error) {
cmdCtxLogger.Info().Str("package", command.PackageName).Msg("Checking package versions")
// Prepare command arguments
ArgsStr := command.Cmd
for _, v := range command.Args {
ArgsStr += fmt.Sprintf(" %s", v)
}
var err error
var cmdOut []byte
if cmdOut, err = commandSession.CombinedOutput(ArgsStr); err != nil {
cmdOutBuf.Write(cmdOut)
_, parseErr := parsePackageVersion(string(cmdOut), cmdCtxLogger, command, cmdOutBuf)
if parseErr != nil {
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.GetOutput), fmt.Errorf("error: package %s not listed: %w", command.PackageName, err)
}
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.GetOutput), fmt.Errorf("error running %s: %w", ArgsStr, err)
}
return parsePackageVersion(string(cmdOut), cmdCtxLogger, command, cmdOutBuf)
} }
// getCommandTypeLabel returns a human-readable label for the command type. // getCommandTypeLabel returns a human-readable label for the command type.
@ -563,20 +609,6 @@ func getCommandTypeLabel(commandType string) string {
return fmt.Sprintf("%s command", commandType) return fmt.Sprintf("%s command", commandType)
} }
// createSSHSession attempts to create a new SSH session and retries on failure.
func (command *Command) createSSHSession(opts *ConfigOpts) (*ssh.Session, error) {
session, err := command.RemoteHost.SshClient.NewSession()
if err == nil {
return session, nil
}
// Retry connection and session creation
if connErr := command.RemoteHost.ConnectToHost(opts); connErr != nil {
return nil, fmt.Errorf("session creation failed: %v, connection retry failed: %v", err, connErr)
}
return command.RemoteHost.SshClient.NewSession()
}
// runScript handles the execution of inline scripts. // runScript handles the execution of inline scripts.
func (command *Command) runScript(session *ssh.Session, cmdCtxLogger zerolog.Logger, outputBuf *bytes.Buffer) ([]string, error) { func (command *Command) runScript(session *ssh.Session, cmdCtxLogger zerolog.Logger, outputBuf *bytes.Buffer) ([]string, error) {
script, err := command.prepareScriptBuffer() script, err := command.prepareScriptBuffer()
@ -590,10 +622,10 @@ func (command *Command) runScript(session *ssh.Session, cmdCtxLogger zerolog.Log
} }
if err := session.Wait(); err != nil { if err := session.Wait(); err != nil {
return collectOutput(outputBuf, command.Name, cmdCtxLogger), fmt.Errorf("error waiting for shell: %w", err) return collectOutput(outputBuf, command.Name, cmdCtxLogger, true), fmt.Errorf("error waiting for shell: %w", err)
} }
return collectOutput(outputBuf, command.Name, cmdCtxLogger), nil return collectOutput(outputBuf, command.Name, cmdCtxLogger, command.GetOutput), nil
} }
// runScriptFile handles the execution of script files. // runScriptFile handles the execution of script files.
@ -609,10 +641,10 @@ func (command *Command) runScriptFile(session *ssh.Session, cmdCtxLogger zerolog
} }
if err := session.Wait(); err != nil { if err := session.Wait(); err != nil {
return collectOutput(outputBuf, command.Name, cmdCtxLogger), fmt.Errorf("error waiting for shell: %w", err) return collectOutput(outputBuf, command.Name, cmdCtxLogger, true), fmt.Errorf("error waiting for shell: %w", err)
} }
return collectOutput(outputBuf, command.Name, cmdCtxLogger), nil return collectOutput(outputBuf, command.Name, cmdCtxLogger, true), nil
} }
// prepareScriptBuffer prepares a buffer for inline scripts. // prepareScriptBuffer prepares a buffer for inline scripts.
@ -677,13 +709,60 @@ func readFileToBuffer(filePath string) (*bytes.Buffer, error) {
} }
// collectOutput collects output from a buffer and logs it. // collectOutput collects output from a buffer and logs it.
func collectOutput(buf *bytes.Buffer, commandName string, logger zerolog.Logger) []string { func collectOutput(buf *bytes.Buffer, commandName string, logger zerolog.Logger, wantOutput bool) []string {
var outputArr []string var outputArr []string
scanner := bufio.NewScanner(buf) scanner := bufio.NewScanner(buf)
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
outputArr = append(outputArr, line) outputArr = append(outputArr, line)
if wantOutput {
logger.Info().Str("cmd", commandName).Str("output", line).Send() logger.Info().Str("cmd", commandName).Str("output", line).Send()
} }
}
return outputArr return outputArr
} }
// createSSHSession attempts to create a new SSH session and retries on failure.
func (h *Host) createSSHSession(opts *ConfigOpts) (*ssh.Session, error) {
session, err := h.SshClient.NewSession()
if err == nil {
return session, nil
}
// Retry connection and session creation
if connErr := h.ConnectToHost(opts); connErr != nil {
return nil, fmt.Errorf("session creation failed: %v, connection retry failed: %v", err, connErr)
}
return h.SshClient.NewSession()
}
func (h *Host) DetectOS(opts *ConfigOpts) (string, error) {
err := h.ConnectToHost(opts)
if err != nil {
return "", err
}
var session *ssh.Session
session, err = h.createSSHSession(opts)
if err != nil {
return "", err
}
// Execute the "uname -a" command on the remote machine
output, err := session.CombinedOutput("uname")
if err != nil {
return "", fmt.Errorf("failed to execute OS detection command: %v", err)
}
// Parse the output to determine the OS
osName := string(output)
return osName, nil
}
func CheckIfHostHasHostName(host string) (bool, string) {
HostName, err := ssh_config.DefaultUserSettings.GetStrict(host, "HostName")
if err != nil {
return false, ""
}
println(HostName)
return HostName != "", HostName
}

View File

@ -1,12 +1,12 @@
Command list {{.listName }} failed. Command list {{.listName }} failed.
The command run was {{.Cmd}}. The command run was {{.CmdName}}.
The command executed was {{.Command}} {{ if .Args }} {{- range .Args}} {{.}} {{end}} {{end}} The command executed was {{.Command}} {{ if .Args }} {{- range .Args}} {{.}} {{end}} {{end}}
{{ if .Err }} The error was {{ .Err }}{{ end }} {{ if .Err }} The error was {{ .Err }}{{ end }}
{{ if .Output }} The output was {{- range .Output}} {{.}} {{end}} {{end}} {{ if .Output }} The output was: {{- range .Output}} {{.}} {{end}} {{end}}
{{ if .CmdsRan }} {{ if .CmdsRan }}
The following commands ran: The following commands ran:
@ -15,7 +15,7 @@ The following commands ran:
{{end}} {{end}}
{{ end }} {{ end }}
{{ if .CmdOutput }}{{- range .CmdOutput }}Command output for {{ .CmdName }}: {{ if .CmdOutput }}{{- range .CmdOutput }}{{ printf "\n"}}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 }}Command output for {{ .CmdName }}: {{ if .CmdOutput }}{{- range .CmdOutput }}{{ printf "\n"}}Command output for {{ .CmdName }}:
{{- range .Output}} {{- range .Output}}
{{ . }} {{ . }}
{{ end }}{{ end }} {{ end }}{{ end }}

View File

@ -4,7 +4,11 @@ import (
"bytes" "bytes"
"text/template" "text/template"
"strings"
"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman" "git.andrewnw.xyz/CyberShell/backy/pkg/pkgman"
"git.andrewnw.xyz/CyberShell/backy/pkg/remotefetcher"
"git.andrewnw.xyz/CyberShell/backy/pkg/usermanager"
vaultapi "github.com/hashicorp/vault/api" vaultapi "github.com/hashicorp/vault/api"
"github.com/kevinburke/ssh_config" "github.com/kevinburke/ssh_config"
"github.com/knadh/koanf/v2" "github.com/knadh/koanf/v2"
@ -18,6 +22,7 @@ type (
// 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
Host struct { Host struct {
OS string `yaml:"OS,omitempty"`
ConfigFilePath string `yaml:"config,omitempty"` ConfigFilePath string `yaml:"config,omitempty"`
Host string `yaml:"host,omitempty"` Host string `yaml:"host,omitempty"`
HostName string `yaml:"hostname,omitempty"` HostName string `yaml:"hostname,omitempty"`
@ -62,10 +67,7 @@ type (
// hook refs are internal references of commands for each hook type // hook refs are internal references of commands for each hook type
hookRefs map[string]map[string]*Command 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.
*/
Shell string `yaml:"shell,omitempty"` Shell string `yaml:"shell,omitempty"`
RemoteHost *Host `yaml:"-"` RemoteHost *Host `yaml:"-"`
@ -86,11 +88,14 @@ type (
Environment []string `yaml:"environment,omitempty"` Environment []string `yaml:"environment,omitempty"`
// Output determines if output is requested. // Output determines if output is requested.
// Only works if command is in a list. //
// Only for when command is in a list.
GetOutput bool `yaml:"getOutput,omitempty"` GetOutput bool `yaml:"getOutput,omitempty"`
ScriptEnvFile string `yaml:"scriptEnvFile"` ScriptEnvFile string `yaml:"scriptEnvFile"`
// BEGIN PACKAGE COMMAND FIELDS
PackageManager string `yaml:"packageManager,omitempty"` PackageManager string `yaml:"packageManager,omitempty"`
PackageName string `yaml:"packageName,omitempty"` PackageName string `yaml:"packageName,omitempty"`
@ -104,6 +109,7 @@ type (
pkgMan pkgman.PackageManager pkgMan pkgman.PackageManager
packageCmdSet bool packageCmdSet bool
// END PACKAGE COMMAND FIELDS
// RemoteSource specifies a URL to fetch the command or configuration remotely // RemoteSource specifies a URL to fetch the command or configuration remotely
RemoteSource string `yaml:"remoteSource,omitempty"` RemoteSource string `yaml:"remoteSource,omitempty"`
@ -111,28 +117,47 @@ type (
// FetchBeforeExecution determines if the remoteSource should be fetched before running // FetchBeforeExecution determines if the remoteSource should be fetched before running
FetchBeforeExecution bool `yaml:"fetchBeforeExecution,omitempty"` FetchBeforeExecution bool `yaml:"fetchBeforeExecution,omitempty"`
// BEGIN USER COMMAND FIELDS
// Username specifies the username for user creation or related operations // Username specifies the username for user creation or related operations
Username string `yaml:"username,omitempty"` Username string `yaml:"userName,omitempty"`
// Groups specifies the groups to add the user to UserID string `yaml:"userID,omitempty"`
Groups []string `yaml:"groups,omitempty"`
// Home specifies the home directory for the user // UserGroups specifies the groups to add the user to
Home string `yaml:"home,omitempty"` UserGroups []string `yaml:"userGroups,omitempty"`
// System specifies whether the user is a system account // UserHome specifies the home directory for the user
System bool `yaml:"system,omitempty"` UserHome string `yaml:"userHome,omitempty"`
// Password specifies the password for the user (can be file: or plain text) // UserShell specifies the shell for the user
Password string `yaml:"password,omitempty"` UserShell string `yaml:"userShell,omitempty"`
// Operation specifies the action for user-related commands (e.g., "create" or "remove") // SystemUser specifies whether the user is a system account
Operation string `yaml:"operation,omitempty"` SystemUser bool `yaml:"systemUser,omitempty"`
// UserPassword specifies the password for the user (can be file: or plain text)
UserPassword string `yaml:"userPassword,omitempty"`
userMan usermanager.UserManager
// OS for the command, only used when type is user
OS string `yaml:"OS,omitempty"`
// UserOperation specifies the action for user-related commands (e.g., "create" or "remove")
UserOperation string `yaml:"userOperation,omitempty"`
userCmdSet bool
// stdin only for userOperation = password (for now)
stdin *strings.Reader
// END USER STRUCT FIELDS
} }
RemoteSource struct { RemoteSource struct {
URL string `yaml:"url"` URL string `yaml:"url"`
Type string `yaml:"type"` // e.g., yaml Type string `yaml:"type"` // e.g., s3, http
Auth struct { Auth struct {
AccessKey string `yaml:"accessKey"` AccessKey string `yaml:"accessKey"`
SecretKey string `yaml:"secretKey"` SecretKey string `yaml:"secretKey"`
@ -162,7 +187,7 @@ type (
// 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-lists"` CmdConfigLists map[string]*CmdList `yaml:"cmdLists"`
// Hosts holds the Host config. // Hosts holds the Host config.
// key is the host. // key is the host.
@ -176,6 +201,9 @@ type (
// Holds config file // Holds config file
ConfigFilePath string ConfigFilePath string
// Holds log file
LogFilePath string
// for command list file // for command list file
CmdListFile string CmdListFile string
@ -198,6 +226,9 @@ type (
koanf *koanf.Koanf koanf *koanf.Koanf
NotificationConf *Notifications `yaml:"notifications"` NotificationConf *Notifications `yaml:"notifications"`
Cache *remotefetcher.Cache
CachedData []*remotefetcher.CacheData
} }
outStruct struct { outStruct struct {
@ -252,10 +283,9 @@ type (
Final []string `yaml:"final,omitempty"` Final []string `yaml:"final,omitempty"`
} }
CmdListResults struct { CmdResult struct {
// name of the list CmdName string // Name of the command executed
ListName string ListName string // Name of the command list
// command that caused the list to fail Error error // Error encountered, if any
ErrCmd string
} }
) )

View File

@ -5,6 +5,7 @@
package backy package backy
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@ -42,7 +43,7 @@ func AddCommandLists(lists []string) BackyOptionFunc {
} }
} }
// AddPrintLists adds lists to print out // SetListsToSearch adds lists to search
func SetListsToSearch(lists []string) BackyOptionFunc { func SetListsToSearch(lists []string) BackyOptionFunc {
return func(bco *ConfigOpts) { return func(bco *ConfigOpts) {
bco.List.Lists = append(bco.List.Lists, lists...) bco.List.Lists = append(bco.List.Lists, lists...)
@ -56,8 +57,15 @@ func SetCmdsToSearch(cmds []string) BackyOptionFunc {
} }
} }
// cronEnabled enables the execution of command lists at specified times // SetLogFile sets the path to the log file
func CronEnabled() BackyOptionFunc { func SetLogFile(logFile string) BackyOptionFunc {
return func(bco *ConfigOpts) {
bco.LogFilePath = logFile
}
}
// EnableCron enables the execution of command lists at specified times
func EnableCron() BackyOptionFunc {
return func(bco *ConfigOpts) { return func(bco *ConfigOpts) {
bco.cronEnabled = true bco.cronEnabled = true
} }
@ -234,9 +242,10 @@ func expandEnvVars(backyEnv map[string]string, envVars []string) {
} }
} }
// getPackageCommand checks for command type of package and if the command has already been set // getCommandType checks for command type and if the command has already been set
// Returns the modified Command with the packageManager command as Cmd and the packageOperation as args, plus any additional Args // Checks for types package and user
func getPackageCommand(command *Command) *Command { // Returns the modified Command with the package- or userManager command as Cmd and the package- or userOperation as args, plus any additional Args
func getCommandType(command *Command) *Command {
if command.Type == "package" && !command.packageCmdSet { if command.Type == "package" && !command.packageCmdSet {
command.packageCmdSet = true command.packageCmdSet = true
@ -247,9 +256,69 @@ func getPackageCommand(command *Command) *Command {
command.Cmd, command.Args = command.pkgMan.Remove(command.PackageName, command.Args) command.Cmd, command.Args = command.pkgMan.Remove(command.PackageName, command.Args)
case "upgrade": case "upgrade":
command.Cmd, command.Args = command.pkgMan.Upgrade(command.PackageName, command.PackageVersion) command.Cmd, command.Args = command.pkgMan.Upgrade(command.PackageName, command.PackageVersion)
case "checkVersion":
command.Cmd, command.Args = command.pkgMan.CheckVersion(command.PackageName, command.PackageVersion)
} }
} else if command.Type != "package" {
command.packageCmdSet = false
} }
if command.Type == "user" && !command.userCmdSet {
command.userCmdSet = true
switch command.UserOperation {
case "add":
command.Cmd, command.Args = command.userMan.AddUser(
command.Username,
command.UserHome,
command.UserShell,
command.SystemUser,
command.UserGroups,
command.Args)
case "modify":
command.Cmd, command.Args = command.userMan.ModifyUser(
command.Username,
command.UserHome,
command.UserShell,
command.UserGroups)
case "checkIfExists":
command.Cmd, command.Args = command.userMan.UserExists(command.Username)
case "delete":
command.Cmd, command.Args = command.userMan.RemoveUser(command.Username)
case "password":
command.Cmd, command.stdin, command.UserPassword = command.userMan.ModifyPassword(command.Username, command.UserPassword)
}
}
return command return command
} }
func parsePackageVersion(output string, cmdCtxLogger zerolog.Logger, command *Command, cmdOutBuf bytes.Buffer) ([]string, error) {
var err error
pkgVersion, err := command.pkgMan.Parse(output)
// println(output)
if err != nil {
cmdCtxLogger.Error().Err(err).Str("package", command.PackageName).Msg("Error parsing package version output")
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.GetOutput), err
}
cmdCtxLogger.Info().
Str("Installed", pkgVersion.Installed).
Str("Candidate", pkgVersion.Candidate).
Msg("Package version comparison")
if command.PackageVersion != "" {
if pkgVersion.Installed == command.PackageVersion {
cmdCtxLogger.Info().Msgf("Installed version matches specified version: %s", command.PackageVersion)
} else {
cmdCtxLogger.Info().Msgf("Installed version does not match specified version: %s", command.PackageVersion)
err = fmt.Errorf("Installed version does not match specified version: %s", command.PackageVersion)
}
} else {
if pkgVersion.Installed == pkgVersion.Candidate {
cmdCtxLogger.Info().Msg("Installed and Candidate versions match")
} else {
cmdCtxLogger.Info().Msg("Installed and Candidate versions differ")
err = errors.New("Installed and Candidate versions differ")
}
}
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, false), err
}

View File

@ -2,6 +2,8 @@ package apt
import ( import (
"fmt" "fmt"
"regexp"
"strings"
"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/pkgcommon" "git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/pkgcommon"
) )
@ -10,6 +12,7 @@ import (
type AptManager struct { type AptManager struct {
useAuth bool // Whether to use an authentication command useAuth bool // Whether to use an authentication command
authCommand string // The authentication command, e.g., "sudo" authCommand string // The authentication command, e.g., "sudo"
Parser pkgcommon.PackageParser
} }
// DefaultAuthCommand is the default command used for authentication. // DefaultAuthCommand is the default command used for authentication.
@ -53,7 +56,7 @@ func (a *AptManager) Remove(pkg string, args []string) (string, []string) {
// Upgrade returns the command and arguments for upgrading a specific package. // Upgrade returns the command and arguments for upgrading a specific package.
func (a *AptManager) Upgrade(pkg, version string) (string, []string) { func (a *AptManager) Upgrade(pkg, version string) (string, []string) {
baseCmd := a.prependAuthCommand(DefaultPackageCommand) baseCmd := a.prependAuthCommand(DefaultPackageCommand)
baseArgs := []string{"update", "&&", baseCmd, "install", "--only-upgrade", "-y "} baseArgs := []string{"update", "&&", baseCmd, "install", "--only-upgrade", "-y"}
if version != "" { if version != "" {
baseArgs = append(baseArgs, fmt.Sprintf("%s=%s", pkg, version)) baseArgs = append(baseArgs, fmt.Sprintf("%s=%s", pkg, version))
} else { } else {
@ -62,6 +65,14 @@ func (a *AptManager) Upgrade(pkg, version string) (string, []string) {
return baseCmd, baseArgs return baseCmd, baseArgs
} }
// CheckVersion returns the command and arguments for checking the info of a specific package.
func (a *AptManager) CheckVersion(pkg, version string) (string, []string) {
baseCmd := a.prependAuthCommand("apt-cache")
baseArgs := []string{"policy", pkg}
return baseCmd, baseArgs
}
// UpgradeAll returns the command and arguments for upgrading all packages. // UpgradeAll returns the command and arguments for upgrading all packages.
func (a *AptManager) UpgradeAll() (string, []string) { func (a *AptManager) UpgradeAll() (string, []string) {
baseCmd := a.prependAuthCommand(DefaultPackageCommand) baseCmd := a.prependAuthCommand(DefaultPackageCommand)
@ -93,3 +104,27 @@ func (a *AptManager) SetUseAuth(useAuth bool) {
func (a *AptManager) SetAuthCommand(authCommand string) { func (a *AptManager) SetAuthCommand(authCommand string) {
a.authCommand = authCommand a.authCommand = authCommand
} }
// Parse parses the apt-cache policy output to extract Installed and Candidate versions.
func (a *AptManager) Parse(output string) (*pkgcommon.PackageVersion, error) {
// Check for error message in the output
if strings.Contains(output, "Unable to locate package") {
return nil, fmt.Errorf("error: %s", strings.TrimSpace(output))
}
reInstalled := regexp.MustCompile(`Installed:\s*([^\s]+)`)
reCandidate := regexp.MustCompile(`Candidate:\s*([^\s]+)`)
installedMatch := reInstalled.FindStringSubmatch(output)
candidateMatch := reCandidate.FindStringSubmatch(output)
if len(installedMatch) < 2 || len(candidateMatch) < 2 {
return nil, fmt.Errorf("failed to parse Installed or Candidate versions from apt output. check package name")
}
return &pkgcommon.PackageVersion{
Installed: strings.TrimSpace(installedMatch[1]),
Candidate: strings.TrimSpace(candidateMatch[1]),
Match: installedMatch[1] == candidateMatch[1],
}, nil
}

View File

@ -2,6 +2,8 @@ package dnf
import ( import (
"fmt" "fmt"
"regexp"
"strings"
"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/pkgcommon" "git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/pkgcommon"
) )
@ -74,6 +76,50 @@ func (y *DnfManager) UpgradeAll() (string, []string) {
return baseCmd, baseArgs return baseCmd, baseArgs
} }
// CheckVersion returns the command and arguments for checking the info of a specific package.
func (d *DnfManager) CheckVersion(pkg, version string) (string, []string) {
baseCmd := d.prependAuthCommand("dnf")
baseArgs := []string{"info", pkg}
return baseCmd, baseArgs
}
// Parse parses the dnf info output to extract Installed and Candidate versions.
func (d DnfManager) Parse(output string) (*pkgcommon.PackageVersion, error) {
// Check for error message in the output
if strings.Contains(output, "No matching packages to list") {
return nil, fmt.Errorf("error: package not listed")
}
// Define regular expressions to capture installed and available versions
reInstalled := regexp.MustCompile(`(?m)^Installed packages\s*Name\s*:\s*\S+\s*Epoch\s*:\s*\S+\s*Version\s*:\s*([^\s]+)\s*Release\s*:\s*([^\s]+)`)
reAvailable := regexp.MustCompile(`(?m)^Available packages\s*Name\s*:\s*\S+\s*Epoch\s*:\s*\S+\s*Version\s*:\s*([^\s]+)\s*Release\s*:\s*([^\s]+)`)
installedMatch := reInstalled.FindStringSubmatch(output)
candidateMatch := reAvailable.FindStringSubmatch(output)
installedVersion := ""
candidateVersion := ""
if len(installedMatch) >= 3 {
installedVersion = fmt.Sprintf("%s-%s", installedMatch[1], installedMatch[2])
}
if len(candidateMatch) >= 3 {
candidateVersion = fmt.Sprintf("%s-%s", candidateMatch[1], candidateMatch[2])
}
if installedVersion == "" && candidateVersion == "" {
return nil, fmt.Errorf("failed to parse versions from dnf output")
}
return &pkgcommon.PackageVersion{
Installed: installedVersion,
Candidate: candidateVersion,
}, nil
}
// prependAuthCommand prepends the authentication command if UseAuth is true. // prependAuthCommand prepends the authentication command if UseAuth is true.
func (y *DnfManager) prependAuthCommand(baseCmd string) string { func (y *DnfManager) prependAuthCommand(baseCmd string) string {
if y.useAuth { if y.useAuth {

View File

@ -2,3 +2,16 @@ package pkgcommon
// PackageManagerOption defines a functional option for configuring a PackageManager. // PackageManagerOption defines a functional option for configuring a PackageManager.
type PackageManagerOption func(interface{}) type PackageManagerOption func(interface{})
// PackageParser defines an interface for parsing package version information.
type PackageParser interface {
Parse(output string) (*PackageVersion, error)
}
// PackageVersion represents the installed and candidate versions of a package.
type PackageVersion struct {
Installed string
Candidate string
Match bool
Message string
}

View File

@ -15,7 +15,8 @@ type PackageManager interface {
Remove(pkg string, args []string) (string, []string) Remove(pkg string, args []string) (string, []string)
Upgrade(pkg, version string) (string, []string) // Upgrade a specific package Upgrade(pkg, version string) (string, []string) // Upgrade a specific package
UpgradeAll() (string, []string) UpgradeAll() (string, []string)
CheckVersion(pkg, version string) (string, []string)
Parse(output string) (*pkgcommon.PackageVersion, error)
// Configure applies functional options to customize the package manager. // Configure applies functional options to customize the package manager.
Configure(options ...pkgcommon.PackageManagerOption) Configure(options ...pkgcommon.PackageManagerOption)
} }
@ -67,6 +68,8 @@ func WithoutAuth() pkgcommon.PackageManagerOption {
// ConfigurablePackageManager defines methods for setting configuration options. // ConfigurablePackageManager defines methods for setting configuration options.
type ConfigurablePackageManager interface { type ConfigurablePackageManager interface {
pkgcommon.PackageParser
SetUseAuth(useAuth bool) SetUseAuth(useAuth bool)
SetAuthCommand(authCommand string) SetAuthCommand(authCommand string)
SetPackageParser(parser pkgcommon.PackageParser)
} }

View File

@ -2,6 +2,7 @@ package yum
import ( import (
"fmt" "fmt"
"regexp"
"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/pkgcommon" "git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/pkgcommon"
) )
@ -74,6 +75,43 @@ func (y *YumManager) UpgradeAll() (string, []string) {
return baseCmd, baseArgs return baseCmd, baseArgs
} }
// CheckVersion returns the command and arguments for checking the info of a specific package.
func (y *YumManager) CheckVersion(pkg, version string) (string, []string) {
baseCmd := y.prependAuthCommand("yum")
baseArgs := []string{"info", pkg}
return baseCmd, baseArgs
}
// Parse parses the dnf info output to extract Installed and Candidate versions.
func (y YumManager) Parse(output string) (*pkgcommon.PackageVersion, error) {
reInstalled := regexp.MustCompile(`(?m)^Installed Packages\s*Name\s*:\s*\S+\s*Version\s*:\s*([^\s]+)\s*Release\s*:\s*([^\s]+)`)
reAvailable := regexp.MustCompile(`(?m)^Available Packages\s*Name\s*:\s*\S+\s*Version\s*:\s*([^\s]+)\s*Release\s*:\s*([^\s]+)`)
installedMatch := reInstalled.FindStringSubmatch(output)
candidateMatch := reAvailable.FindStringSubmatch(output)
installedVersion := ""
candidateVersion := ""
if len(installedMatch) >= 3 {
installedVersion = fmt.Sprintf("%s-%s", installedMatch[1], installedMatch[2])
}
if len(candidateMatch) >= 3 {
candidateVersion = fmt.Sprintf("%s-%s", candidateMatch[1], candidateMatch[2])
}
if installedVersion == "" && candidateVersion == "" {
return nil, fmt.Errorf("failed to parse versions from dnf output")
}
return &pkgcommon.PackageVersion{
Installed: installedVersion,
Candidate: candidateVersion,
}, nil
}
// prependAuthCommand prepends the authentication command if UseAuth is true. // prependAuthCommand prepends the authentication command if UseAuth is true.
func (y *YumManager) prependAuthCommand(baseCmd string) string { func (y *YumManager) prependAuthCommand(baseCmd string) string {
if y.useAuth { if y.useAuth {

186
pkg/remotefetcher/cache.go Normal file
View File

@ -0,0 +1,186 @@
package remotefetcher
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"os"
"path/filepath"
"sync"
"gopkg.in/yaml.v3"
)
type CacheData struct {
Hash string `yaml:"hash"`
Path string `yaml:"path"`
Type string `yaml:"type"`
}
type Cache struct {
mu sync.Mutex
store map[string]CacheData
file string
dir string
}
func NewCache(file, dir string) (*Cache, error) {
cache := &Cache{
store: make(map[string]CacheData),
file: file,
dir: dir,
}
err := cache.loadFromFile()
if err != nil {
return nil, err
}
return cache, nil
}
func (c *Cache) loadFromFile() error {
c.mu.Lock()
defer c.mu.Unlock()
if _, err := os.Stat(c.file); os.IsNotExist(err) {
return nil
}
data, err := os.ReadFile(c.file)
if err != nil {
return err
}
var cacheData []CacheData
err = yaml.Unmarshal(data, &cacheData)
if err != nil {
return err
}
for _, item := range cacheData {
c.store[item.Hash] = item
}
return nil
}
func (c *Cache) saveToFile() error {
c.mu.Lock()
defer c.mu.Unlock()
var cacheData []CacheData
for _, data := range c.store {
cacheData = append(cacheData, data)
}
data, err := yaml.Marshal(cacheData)
if err != nil {
return err
}
return os.WriteFile(c.file, data, 0644)
}
func (c *Cache) Get(hash string) ([]byte, CacheData, bool) {
c.mu.Lock()
defer c.mu.Unlock()
cacheData, exists := c.store[hash]
if !exists {
return nil, CacheData{}, false
}
data, err := os.ReadFile(cacheData.Path)
if err != nil {
return nil, CacheData{}, false
}
return data, cacheData, true
}
func (c *Cache) AddDataToStore(hash string, cacheData CacheData) error {
c.store[hash] = cacheData
return c.saveToFile()
}
func (c *Cache) Set(source, hash string, data []byte, dataType string) (CacheData, error) {
c.mu.Lock()
defer c.mu.Unlock()
fileName := filepath.Base(source)
path := filepath.Join(c.dir, fmt.Sprintf("%s-%s", fileName, hash))
if _, err := os.Stat(path); os.IsNotExist(err) {
os.MkdirAll(c.dir, 0700)
}
err := os.WriteFile(path, data, 0644)
if err != nil {
return CacheData{}, err
}
cacheData := CacheData{
Hash: hash,
Path: path,
Type: dataType,
}
c.store[hash] = cacheData
// Unlock before calling saveToFile to avoid double-locking
c.mu.Unlock()
err = c.saveToFile()
c.mu.Lock()
if err != nil {
return CacheData{}, err
}
// fmt.Printf("Cache data: %v", cacheData)
return cacheData, nil
}
type CachedFetcher struct {
data []byte
path string
dataType string
}
func (cf *CachedFetcher) Fetch(source string) ([]byte, error) {
return cf.data, nil
}
func (cf *CachedFetcher) Parse(data []byte, target interface{}) error {
if cf.dataType == "yaml" {
return yaml.Unmarshal(data, target)
}
return errors.New("parse not supported on cached fetcher for scripts")
}
func (cf *CachedFetcher) Hash(data []byte) string {
hash := sha256.Sum256(data)
return hex.EncodeToString(hash[:])
}
// Function to read and parse the metadata file
func LoadMetadataFromFile(filePath string) ([]*CacheData, error) {
if _, err := os.Stat(filePath); os.IsNotExist(err) {
// Create the file if it does not exist
emptyData := []byte("[]")
err := os.WriteFile(filePath, emptyData, 0644)
if err != nil {
return nil, err
}
return nil, nil
}
data, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}
var cacheData []*CacheData
err = yaml.Unmarshal(data, &cacheData)
if err != nil {
return nil, err
}
return cacheData, nil
}

View File

@ -0,0 +1,76 @@
package remotefetcher
import (
"errors"
"strings"
)
type RemoteFetcher interface {
// Fetch retrieves the configuration from the specified URL or source
// Returns the raw data as bytes or an error
Fetch(source string) ([]byte, error)
// Parse decodes the raw data into a Go structure (e.g., Commands, CommandLists)
// Takes the raw data as input and populates the target interface
Parse(data []byte, target interface{}) error
// Hash returns the hash of the configuration data
Hash(data []byte) string
}
// ErrIgnoreFileNotFound is returned when the file is not found and should be ignored
var ErrIgnoreFileNotFound = errors.New("remotefetcher: file not found")
func NewRemoteFetcher(source string, cache *Cache, options ...FetcherOption) (RemoteFetcher, error) {
var fetcher RemoteFetcher
config := FetcherConfig{}
for _, option := range options {
option(&config)
}
// If FileType is empty (i.e. WithFileType was not called), yaml is the default file type
if strings.TrimSpace(config.FileType) == "" {
config.FileType = "yaml"
}
if strings.HasPrefix(source, "http") || strings.HasPrefix(source, "https") {
fetcher = NewHTTPFetcher(options...)
} else if strings.HasPrefix(source, "s3") {
var err error
fetcher, err = NewS3Fetcher(source, options...)
if err != nil {
return nil, err
}
} else {
fetcher = &LocalFetcher{}
return fetcher, nil
}
//TODO: should local files be cached?
data, err := fetcher.Fetch(source)
if err != nil {
if config.IgnoreFileNotFound && isFileNotFoundError(err) {
return nil, ErrIgnoreFileNotFound
}
return nil, err
}
hash := fetcher.Hash(data)
if cachedData, cacheMeta, exists := cache.Get(hash); exists {
return &CachedFetcher{data: cachedData, path: cacheMeta.Path, dataType: cacheMeta.Type}, nil
}
cacheData, err := cache.Set(source, hash, data, config.FileType)
if err != nil {
return nil, err
}
return &CachedFetcher{data: data, path: cacheData.Path, dataType: cacheData.Type}, nil
}
func isFileNotFoundError(err error) bool {
// Implement logic to check if the error is a "file not found" error
// This can be based on the error type or message
return strings.Contains(err.Error(), "file not found")
}

60
pkg/remotefetcher/http.go Normal file
View File

@ -0,0 +1,60 @@
package remotefetcher
import (
"crypto/sha256"
"encoding/hex"
"errors"
"io"
"net/http"
"gopkg.in/yaml.v3"
)
type HTTPFetcher struct {
HTTPClient *http.Client
config FetcherConfig
}
// NewHTTPFetcher creates a new instance of HTTPFetcher with the provided options.
func NewHTTPFetcher(options ...FetcherOption) *HTTPFetcher {
cfg := &FetcherConfig{}
for _, opt := range options {
opt(cfg)
}
// Initialize HTTP client if not provided
if cfg.HTTPClient == nil {
cfg.HTTPClient = http.DefaultClient
}
return &HTTPFetcher{HTTPClient: cfg.HTTPClient, config: *cfg}
}
// Fetch retrieves the file from the specified source URL
func (h *HTTPFetcher) Fetch(source string) ([]byte, error) {
resp, err := http.Get(source)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound && h.config.IgnoreFileNotFound {
return nil, ErrIgnoreFileNotFound
}
if resp.StatusCode != http.StatusOK {
return nil, errors.New("failed to fetch remote config: " + resp.Status)
}
return io.ReadAll(resp.Body)
}
// Parse decodes the raw data into the provided target structure
func (h *HTTPFetcher) Parse(data []byte, target interface{}) error {
return yaml.Unmarshal(data, target)
}
func (h *HTTPFetcher) Hash(data []byte) string {
hash := sha256.Sum256(data)
return hex.EncodeToString(hash[:])
}

View File

@ -0,0 +1,42 @@
package remotefetcher
import (
"crypto/sha256"
"encoding/hex"
"io"
"os"
"gopkg.in/yaml.v3"
)
type LocalFetcher struct {
config FetcherConfig
}
// Fetch retrieves the file from the specified local file path
func (l *LocalFetcher) Fetch(source string) ([]byte, error) {
// Check if the file exists
if _, err := os.Stat(source); os.IsNotExist(err) {
if l.config.IgnoreFileNotFound {
return nil, ErrIgnoreFileNotFound
}
return nil, nil
}
file, err := os.Open(source)
if err != nil {
return nil, err
}
defer file.Close()
return io.ReadAll(file)
}
// Parse decodes the raw data into the provided target structure
func (l *LocalFetcher) Parse(data []byte, target interface{}) error {
return yaml.Unmarshal(data, target)
}
func (l *LocalFetcher) Hash(data []byte) string {
hash := sha256.Sum256(data)
return hex.EncodeToString(hash[:])
}

View File

@ -0,0 +1,49 @@
package remotefetcher
import (
"net/http"
"strings"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
// Option is a function that configures a fetcher.
type FetcherOption func(*FetcherConfig)
// FetcherConfig holds the configuration for a fetcher.
type FetcherConfig struct {
S3Client *s3.Client
HTTPClient *http.Client
FileType string
IgnoreFileNotFound bool
}
// WithS3Client sets the S3 client for the fetcher.
func WithS3Client(client *s3.Client) FetcherOption {
return func(cfg *FetcherConfig) {
cfg.S3Client = client
}
}
// WithHTTPClient sets the HTTP client for the fetcher.
func WithHTTPClient(client *http.Client) FetcherOption {
return func(cfg *FetcherConfig) {
cfg.HTTPClient = client
}
}
func IgnoreFileNotFound() FetcherOption {
return func(cfg *FetcherConfig) {
cfg.IgnoreFileNotFound = true
}
}
// WithFileType ensures the default FileType will be yaml
func WithFileType(fileType string) FetcherOption {
return func(cfg *FetcherConfig) {
cfg.FileType = fileType
if strings.TrimSpace(fileType) == "" {
cfg.FileType = "yaml"
}
}
}

162
pkg/remotefetcher/s3.go Normal file
View File

@ -0,0 +1,162 @@
package remotefetcher
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"io"
"net/http"
"net/url"
"os"
"path"
"strings"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/mitchellh/go-homedir"
"gopkg.in/yaml.v3"
)
type S3Fetcher struct {
S3Client *minio.Client
config FetcherConfig
}
// NewS3Fetcher creates a new instance of S3Fetcher with the provided options.
func NewS3Fetcher(endpoint string, options ...FetcherOption) (*S3Fetcher, error) {
cfg := &FetcherConfig{}
var s3Client *minio.Client
var err error
for _, opt := range options {
opt(cfg)
}
/*
options for S3 urls:
1. s3://bucket.region.endpoint.tld/path/to/object
2. alias with path and rest is looked up in file - add FetcherOptions
options for S3 credentials:
1. from file ($HOME/.aws/credentials)
2. env vars (AWS_SECRET_KEY, etc.)
*/
s3Endpoint := os.Getenv("S3_ENDPOINT")
creds, err := getS3Credentials("default", s3Endpoint, cfg.HTTPClient)
if err != nil {
println(err.Error())
return nil, err
}
// Initialize S3 client if not provided
if cfg.S3Client == nil {
s3Client, err = minio.New(s3Endpoint, &minio.Options{
Creds: creds,
Secure: true,
})
if err != nil {
return nil, err
}
}
return &S3Fetcher{S3Client: s3Client, config: *cfg}, nil
}
// Fetch retrieves the configuration from an S3 bucket
// Source should be in the format "bucket-name/object-key"
func (s *S3Fetcher) Fetch(source string) ([]byte, error) {
bucket, object, err := parseS3Source(source)
if err != nil {
return nil, err
}
doesObjectExist, objErr := objectExists(bucket, object, s.S3Client)
if !doesObjectExist {
if objErr != nil {
return nil, err
}
if s.config.IgnoreFileNotFound {
return nil, ErrIgnoreFileNotFound
}
}
fileObject, err := s.S3Client.GetObject(context.TODO(), bucket, object, minio.GetObjectOptions{})
if err != nil {
println(err.Error())
return nil, err
}
defer fileObject.Close()
fileObjectStats, statErr := fileObject.Stat()
if statErr != nil {
return nil, statErr
}
buffer := make([]byte, fileObjectStats.Size)
// Read the object into the buffer
_, err = io.ReadFull(fileObject, buffer)
if err != nil {
return nil, err
}
return buffer, nil
}
// Parse decodes the raw data into the provided target structure
func (s *S3Fetcher) Parse(data []byte, target interface{}) error {
return yaml.Unmarshal(data, target)
}
// Helper function to parse S3 source into bucket and key
func parseS3Source(source string) (bucket, key string, err error) {
parts := strings.SplitN(source, "/", 2)
if len(parts) != 2 {
return "", "", errors.New("invalid S3 source format, expected bucket-name/object-key")
}
u, _ := url.Parse(source)
u.Path = strings.TrimPrefix(u.Path, "/")
return u.Host, u.Path, nil
}
func (s *S3Fetcher) Hash(data []byte) string {
hash := sha256.Sum256(data)
return hex.EncodeToString(hash[:])
}
func getS3Credentials(profile, host string, httpClient *http.Client) (*credentials.Credentials, error) {
// println(s3utils.GetRegionFromURL(*u))
homeDir, hdirErr := homedir.Dir()
if hdirErr != nil {
return nil, hdirErr
}
s3Creds := credentials.NewFileAWSCredentials(path.Join(homeDir, ".aws", "credentials"), "default")
credVals, credErr := s3Creds.GetWithContext(&credentials.CredContext{Endpoint: host, Client: httpClient})
if credErr != nil {
return nil, credErr
}
creds := credentials.NewStaticV4(credVals.AccessKeyID, credVals.SecretAccessKey, "")
return creds, nil
}
var (
doesNotExist = "The specified key does not exist."
)
// objectExists checks for name in bucket using client.
// It returns false and nil if the key does not exist
func objectExists(bucket, name string, client *minio.Client) (bool, error) {
_, err := client.StatObject(context.TODO(), bucket, name, minio.StatObjectOptions{})
if err != nil {
switch err.Error() {
case doesNotExist:
return false, nil
default:
return false, errors.Join(err, errors.New("error stating object"))
}
}
return true, nil
}

View File

@ -0,0 +1,7 @@
package common
// ConfigurablePackageManager defines methods for setting configuration options.
type ConfigurableUserManager interface {
SetUseAuth(useAuth bool)
SetAuthCommand(authCommand string)
}

View File

@ -0,0 +1,90 @@
package linux
import (
"fmt"
"strings"
passGen "github.com/sethvargo/go-password/password"
)
// LinuxUserManager implements UserManager for Linux systems.
type LinuxUserManager struct{}
func (l LinuxUserManager) NewLinuxManager() *LinuxUserManager {
return &LinuxUserManager{}
}
// AddUser adds a new user to the system.
func (l LinuxUserManager) AddUser(username, homeDir, shell string, isSystem bool, groups, args []string) (string, []string) {
baseArgs := []string{}
if isSystem {
baseArgs = append(baseArgs, "--system")
}
if homeDir != "" {
baseArgs = append(baseArgs, "--home", homeDir)
}
if shell != "" {
baseArgs = append(baseArgs, "--shell", shell)
}
if len(groups) > 0 {
baseArgs = append(baseArgs, "--groups", strings.Join(groups, ","))
}
if len(args) > 0 {
baseArgs = append(baseArgs, args...)
}
args = append(baseArgs, username)
cmd := "useradd"
return cmd, args
}
func (l LinuxUserManager) ModifyPassword(username, password string) (string, *strings.Reader, string) {
cmd := "chpasswd"
if password == "" {
password = passGen.MustGenerate(20, 5, 5, false, false)
}
stdin := strings.NewReader(fmt.Sprintf("%s:%s", username, password))
return cmd, stdin, password
}
// RemoveUser removes an existing user from the system.
func (l LinuxUserManager) RemoveUser(username string) (string, []string) {
cmd := "userdel"
return cmd, []string{username}
}
// ModifyUser modifies an existing user's details.
func (l LinuxUserManager) ModifyUser(username, homeDir, shell string, groups []string) (string, []string) {
args := []string{}
if homeDir != "" {
args = append(args, "--home", homeDir)
}
if shell != "" {
args = append(args, "--shell", shell)
}
if len(groups) > 0 {
args = append(args, "--groups", strings.Join(groups, ","))
}
args = append(args, username)
cmd := "usermod"
return cmd, args
}
// UserExists checks if a user exists on the system.
func (l LinuxUserManager) UserExists(username string) (string, []string) {
cmd := "id"
return cmd, []string{username}
}

View File

@ -0,0 +1,36 @@
package usermanager
import (
"fmt"
"strings"
"git.andrewnw.xyz/CyberShell/backy/pkg/usermanager/linux"
)
// UserManager defines the interface for user management operations.
// All functions but one return a string for the command and any args.
type UserManager interface {
AddUser(username, homeDir, shell string, isSystem bool, groups, args []string) (string, []string)
RemoveUser(username string) (string, []string)
ModifyUser(username, homeDir, shell string, groups []string) (string, []string)
// Modify password uses chpasswd for Linux systems to build the command to change the password
// Should return a password as the last argument
// TODO: refactor when adding more systems instead of Linux
ModifyPassword(username, password string) (string, *strings.Reader, string)
UserExists(username string) (string, []string)
}
// NewUserManager returns a UserManager-compatible struct
func NewUserManager(system string) (UserManager, error) {
var manager UserManager
switch system {
case "linux", "Linux":
manager = linux.LinuxUserManager{}
default:
return nil, fmt.Errorf("usermanger system %s is not recognized", system)
}
return manager, nil
}

11
release
View File

@ -1,6 +1,13 @@
#!/bin/bash #!/bin/bash
# export GORELEASER_CURRENT_TAG="$(go run backy.go version -V)" set -eou pipefail
git tag "$(go run backy.go version -V)" export CURRENT_TAG="$(go run backy.go version -V)"
goreleaser -f .goreleaser/github.yml check
goreleaser -f .goreleaser/gitea.yml check
changie batch $CURRENT_TAG
changie merge
git add .changes/
git commit -am "$CURRENT_TAG"
git tag "$CURRENT_TAG"
git push all git push all
git push all --tags git push all --tags
# goreleaser release -f .goreleaser/gitea.yml --clean --release-notes=".changes/$(go run backy.go version -V).md" # goreleaser release -f .goreleaser/gitea.yml --clean --release-notes=".changes/$(go run backy.go version -V).md"