9 Commits

Author SHA1 Message Date
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
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
48 changed files with 1607 additions and 409 deletions

View File

@ -0,0 +1,3 @@
kind: Added
body: '[feat]: package `packageOperation` option `checkVersion` implemented'
time: 2025-01-11T21:15:46.207199643-06:00

View File

@ -0,0 +1,3 @@
kind: Added
body: user management added - see docs
time: 2025-01-11T21:18:13.182822019-06:00

View File

@ -0,0 +1,3 @@
kind: Added
body: Support for remote config sources. Only config file and list can be used for now.
time: 2025-01-13T23:12:48.383700682-06:00

View File

@ -0,0 +1,3 @@
kind: Added
body: Cache functionality - still a WIP
time: 2025-01-28T15:35:24.512485671-06:00

View File

@ -0,0 +1,3 @@
kind: Changed
body: Internal refactoring of config setup
time: 2025-01-13T23:10:07.215735108-06:00

View File

@ -0,0 +1,3 @@
kind: Changed
body: Formatting and sending for notifications
time: 2025-01-13T23:16:22.260458782-06:00

View File

@ -0,0 +1,3 @@
kind: Changed
body: "name of `configfetcher` to `remotefetcher`"
time: 2025-01-28T15:42:04.282668058-06:00

View File

@ -0,0 +1,3 @@
kind: Fixed
body: Parsing of remote URLs when determining list config file path
time: 2025-01-28T15:38:06.957506929-06:00

View File

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

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
dist/ dist/
.codegpt
.codegpt *.log

View File

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

View File

@ -4,7 +4,8 @@ steps:
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
deploy: deploy:

View File

@ -8,7 +8,7 @@ and is generated by [Changie](https://github.com/miniscruff/changie).
## v0.6.1 - 2025-01-04 ## v0.6.1 - 2025-01-04
### Fixed ### Fixed
* Hooks now run explicitly after the command executes. Fixed panic due to improper logic. * When running a list, hooks now run explicitly after the command executes. Fixed panic due to improper logic.
## v0.6.0 - 2025-01-04 ## v0.6.0 - 2025-01-04
### Added ### Added

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

@ -31,8 +31,7 @@ func init() {
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

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

View File

@ -33,8 +33,8 @@ func execute(cmd *cobra.Command, args []string) {
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

@ -29,10 +29,10 @@ 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 {

View File

@ -31,7 +31,7 @@ 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
@ -42,8 +42,7 @@ func List(cmd *cobra.Command, args []string) {
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,7 @@ var (
// Used for flags. // Used for flags.
cfgFile string cfgFile string
verbose bool verbose bool
logFile string
rootCmd = &cobra.Command{ rootCmd = &cobra.Command{
Use: "backy", Use: "backy",
@ -33,6 +34,8 @@ 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")

View File

@ -14,9 +14,20 @@ If you leave the config path blank, the following paths will be searched in orde
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 get 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.
@ -14,7 +14,7 @@ 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 'cmd-lists.file' is found
2. hosts.yml or hosts.yaml is found in the same directory as the backy config file 2. hosts.yml or hosts.yaml is found in the same directory as the backy config file
```yaml ```yaml {lineNos="true" wrap="true" title="yaml"}
test2: test2:
name: test2 name: test2
order: order:
@ -65,10 +65,10 @@ Backy also has a cron mode, so one can run `backy cron` and start a process that
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 `cmd-lists` 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: cmd-lists:
  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

View File

@ -1,5 +1,6 @@
--- ---
title: "Commands" title: "Commands"
description: Commands are just that, commands
weight: 1 weight: 1
--- ---

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`:

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

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

47
go.mod
View File

@ -1,32 +1,53 @@
module git.andrewnw.xyz/CyberShell/backy module git.andrewnw.xyz/CyberShell/backy
go 1.20 go 1.21
toolchain go1.22.2
replace git.andrewnw.xyz/CyberShell/backy => /home/andrew/Projects/backy replace git.andrewnw.xyz/CyberShell/backy => /home/andrew/Projects/backy
require ( require (
github.com/aws/aws-sdk-go-v2/config v1.18.27
github.com/aws/aws-sdk-go-v2/service/s3 v1.72.0
github.com/go-co-op/gocron v1.33.1 github.com/go-co-op/gocron v1.33.1
github.com/hashicorp/vault/api v1.10.0 github.com/hashicorp/vault/api v1.10.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.0.1
github.com/mattn/go-isatty v0.0.19 github.com/mattn/go-isatty v0.0.19
github.com/nikoksr/notify v0.41.0 github.com/nikoksr/notify v0.41.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.30.0
github.com/sethvargo/go-password v0.3.1
github.com/spf13/cobra v1.7.0 github.com/spf13/cobra v1.7.0
golang.org/x/crypto v0.13.0 golang.org/x/crypto v0.31.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mautrix v0.16.0 maunium.net/go/mautrix v0.16.0
mvdan.cc/sh/v3 v3.7.0 mvdan.cc/sh/v3 v3.7.0
) )
require ( require (
github.com/aws/aws-sdk-go-v2 v1.32.7 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.26 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.26 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.12 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 // indirect
github.com/aws/smithy-go v1.22.1 // indirect
github.com/cenkalti/backoff/v3 v3.2.2 // indirect github.com/cenkalti/backoff/v3 v3.2.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.0 // indirect github.com/go-jose/go-jose/v3 v3.0.0 // indirect
github.com/google/uuid v1.3.1 // indirect github.com/google/uuid v1.3.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
@ -39,6 +60,7 @@ require (
github.com/hashicorp/go-sockaddr v1.0.5 // indirect github.com/hashicorp/go-sockaddr v1.0.5 // 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/jmespath/go-jmespath v0.4.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/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.13 // indirect
@ -48,22 +70,23 @@ require (
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/rogpeppe/go-internal v1.12.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.5 // 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.16.0 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect github.com/tidwall/sjson v1.2.5 // indirect
go.mau.fi/util v0.0.0-20230906155759-14bad39a8718 // indirect go.mau.fi/util v0.0.0-20230906155759-14bad39a8718 // indirect
go.uber.org/atomic v1.11.0 // indirect go.uber.org/atomic v1.11.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/net v0.15.0 // indirect golang.org/x/net v0.33.0 // indirect
golang.org/x/sync v0.3.0 // indirect golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.12.0 // indirect golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.13.0 // indirect golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.3.0 // indirect golang.org/x/time v0.3.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
maunium.net/go/maulogger/v2 v2.4.1 // indirect maunium.net/go/maulogger/v2 v2.4.1 // indirect
) )

103
go.sum
View File

@ -1,4 +1,45 @@
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aws/aws-sdk-go-v2 v1.18.1/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2 v1.32.7 h1:ky5o35oENWi0JYWUZkB7WYvVPP+bcRF5/Iq7JWSb5Rw=
github.com/aws/aws-sdk-go-v2 v1.32.7/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7/go.mod h1:QraP0UcVlQJsmHfioCrveWOC1nbiWUl3ej08h4mXWoc=
github.com/aws/aws-sdk-go-v2/config v1.18.27 h1:Az9uLwmssTE6OGTpsFqOnaGpLnKDqNYOJzWuC6UAYzA=
github.com/aws/aws-sdk-go-v2/config v1.18.27/go.mod h1:0My+YgmkGxeqjXZb5BYme5pc4drjTnM+x1GJ3zv42Nw=
github.com/aws/aws-sdk-go-v2/credentials v1.13.26 h1:qmU+yhKmOCyujmuPY7tf5MxR/RKyZrOPO3V4DobiTUk=
github.com/aws/aws-sdk-go-v2/credentials v1.13.26/go.mod h1:GoXt2YC8jHUBbA4jr+W3JiemnIbkXOfxSXcisUsZ3os=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 h1:LxK/bitrAr4lnh9LnIS6i7zWbCOdMsfzKFBI6LUCS0I=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4/go.mod h1:E1hLXN/BL2e6YizK1zFlYd8vsfi2GTjbjBazinMmeaM=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34/go.mod h1:wZpTEecJe0Btj3IYnDx/VlUzor9wm3fJHyvLpQF0VwY=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26 h1:I/5wmGMffY4happ8NOCuIUEWGUvvFp5NSeQcXl9RHcI=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26/go.mod h1:FR8f4turZtNy6baO0KJ5FJUmXH/cSkI9fOngs0yl6mA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28/go.mod h1:7VRpKQQedkfIEXb4k52I7swUnZP0wohVajJMRn3vsUw=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 h1:zXFLuEuMMUOvEARXFUVJdfqZ4bvvSgdGRq/ATcrQxzM=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26/go.mod h1:3o2Wpy0bogG1kyOPrgkXA8pgIfEEv0+m19O9D5+W8y8=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 h1:LWA+3kDM8ly001vJ1X1waCuLJdtTl48gwkPKWy9sosI=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35/go.mod h1:0Eg1YjxE0Bhn56lx+SHJwCzhW+2JGtizsrx+lCqrfm0=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.26 h1:GeNJsIFHB+WW5ap2Tec4K6dzcVTsRbsT1Lra46Hv9ME=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.26/go.mod h1:zfgMpwHDXX2WGoG84xG2H+ZlPTkJUU4YUvx2svLQYWo=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.7 h1:tB4tNw83KcajNAzaIMhkhVI2Nt8fAZd5A5ro113FEMY=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.7/go.mod h1:lvpyBGkZ3tZ9iSsUIcC2EWp+0ywa7aK3BLT+FwZi+mQ=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28/go.mod h1:jj7znCIg05jXlaGBlFMGP8+7UN3VtCkRBG2spnmRQkU=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7 h1:8eUsivBQzZHqe/3FE+cqwfH+0p5Jo8PFM/QYQSmeZ+M=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7/go.mod h1:kLPQvGUmxn/fqiCrDeohwG33bq2pQpGeY62yRO6Nrh0=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.7 h1:Hi0KGbrnr57bEHWM0bJ1QcBzxLrL/k2DHvGYhb8+W1w=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.7/go.mod h1:wKNgWgExdjjrm4qvfbTorkvocEstaoDl4WCvGfeCy9c=
github.com/aws/aws-sdk-go-v2/service/s3 v1.72.0 h1:SAfh4pNx5LuTafKKWR02Y+hL3A+3TX8cTKG1OIAJaBk=
github.com/aws/aws-sdk-go-v2/service/s3 v1.72.0/go.mod h1:r+xl5yzMk9083rMR+sJ5TYj9Tihvf/l1oxzZXDgGj2Q=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.12 h1:nneMBM2p79PGWBQovYO/6Xnc2ryRMw3InnDJq1FHkSY=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.12/go.mod h1:HuCOxYsF21eKrerARYO6HapNeh9GBNq7fius2AcwodY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 h1:2qTR7IFk7/0IN/adSFhYu9Xthr0zVFTgBrmPldILn80=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12/go.mod h1:E4VrHCPzmVB/KFXtqBGKb3c8zpbNBgKe3fisDNLAW5w=
github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 h1:XFJ2Z6sNUUcAz9poj+245DMkrHE4h2j5I9/xD50RHfE=
github.com/aws/aws-sdk-go-v2/service/sts v1.19.2/go.mod h1:dp0yLPsLBOi++WTxzCjA/oZqi6NPIhoR+uF7GeMU9eg=
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=
github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
@ -11,16 +52,18 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/go-co-op/gocron v1.33.1 h1:wjX+Dg6Ae29a/f9BSQjY1Rl+jflTpW9aDyMqseCj78c= github.com/go-co-op/gocron v1.33.1 h1:wjX+Dg6Ae29a/f9BSQjY1Rl+jflTpW9aDyMqseCj78c=
github.com/go-co-op/gocron v1.33.1/go.mod h1:NLi+bkm4rRSy1F8U7iacZOz0xPseMoIOnvabGoSe/no= github.com/go-co-op/gocron v1.33.1/go.mod h1:NLi+bkm4rRSy1F8U7iacZOz0xPseMoIOnvabGoSe/no=
github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
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.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
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.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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=
@ -30,6 +73,7 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n
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 v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs= github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs=
github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 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=
@ -51,6 +95,10 @@ github.com/hashicorp/vault/api v1.10.0 h1:/US7sIjWN6Imp4o/Rj1Ce2Nr5bki/AXi9vAW3p
github.com/hashicorp/vault/api v1.10.0/go.mod h1:jo5Y/ET+hNyz+JnKDt8XLAdKs+AM0G5W0Vp1IrFI8N8= github.com/hashicorp/vault/api v1.10.0/go.mod h1:jo5Y/ET+hNyz+JnKDt8XLAdKs+AM0G5W0Vp1IrFI8N8=
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/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA= github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
@ -61,14 +109,15 @@ github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NI
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.0.1 h1:1dYGITt1I23x8cfx8ZnldtezdyaZtfAuRtIFOiRzK7g=
github.com/knadh/koanf/v2 v2.0.1/go.mod h1:ZeiIlIDXTE7w1lMT6UVcNiRAS2/rCeLn/GdLNvY1Dus= github.com/knadh/koanf/v2 v2.0.1/go.mod h1:ZeiIlIDXTE7w1lMT6UVcNiRAS2/rCeLn/GdLNvY1Dus=
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=
@ -105,7 +154,8 @@ 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.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
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/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
@ -113,6 +163,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 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/sethvargo/go-password v0.3.1 h1:WqrLTjo7X6AcVYfC6R7GtSyuUQR9hGyAj/f1PYQZCJU=
github.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5OptoxrMBcrXzvs=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@ -120,8 +172,8 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
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.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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@ -129,8 +181,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
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.16.0 h1:SyXa+dsSPpUlcwEDuKuEBJEz5vzTvOea+9rjyYodQFg=
github.com/tidwall/gjson v1.16.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.16.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@ -148,29 +200,29 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -181,6 +233,9 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
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=

View File

@ -45,11 +45,19 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
} }
) )
// 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) if command.Type == "user" {
if command.UserOperation == "password" {
cmdCtxLogger.Info().Str("password", command.UserPassword).Msg("user password to be updated")
}
}
var errSSH error var errSSH error
// is host defined // is host defined
@ -60,53 +68,46 @@ 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
cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s on local machine", command.Name)).Send()
// 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...)
} }
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()
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 +135,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()
@ -173,7 +176,7 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<-
// Notify failure // Notify failure
if list.NotifyConfig != nil { if list.NotifyConfig != nil {
notifyError(cmdLogger, msgTemps, list, cmdsRan, outStructArr, runErr, cmdToRun, opts) notifyError(cmdLogger, msgTemps, list, cmdsRan, outStructArr, runErr, cmdToRun)
} }
hasError = true hasError = true
break break
@ -227,7 +230,7 @@ func cmdsRanContains(cmd string, cmdsRan []string) bool {
} }
// Helper to notify errors // Helper to notify errors
func notifyError(logger zerolog.Logger, templates *msgTemplates, list *CmdList, cmdsRan []string, outStructArr []outStruct, err error, cmd *Command, opts *ConfigOpts) { func notifyError(logger zerolog.Logger, templates *msgTemplates, list *CmdList, cmdsRan []string, outStructArr []outStruct, err error, cmd *Command) {
errStruct := map[string]interface{}{ errStruct := map[string]interface{}{
"listName": list.Name, "listName": list.Name,
"CmdsRan": cmdsRan, "CmdsRan": cmdsRan,
@ -299,13 +302,7 @@ func (opts *ConfigOpts) RunListConfig(cron string) {
opts.closeHostConnections() opts.closeHostConnections()
} }
type CmdResult struct { func (opts *ConfigOpts) ExecuteCmds() {
CmdName string // Name of the command executed
ListName string // Name of the command list
Error error // Error encountered, if any
}
func (config *ConfigOpts) ExecuteCmds(opts *ConfigOpts) {
for _, cmd := range opts.executeCmds { for _, cmd := range opts.executeCmds {
cmdToRun := opts.Cmds[cmd] cmdToRun := opts.Cmds[cmd]
cmdLogger := cmdToRun.GenerateLogger(opts) cmdLogger := cmdToRun.GenerateLogger(opts)
@ -425,3 +422,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,130 @@ 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)
metadataFile := "hashMetadataSample.yml"
cacheDir := homeCacheDir
// Load metadata from file
opts.CachedData, err = remotefetcher.LoadMetadataFromFile(metadataFile)
if err != nil {
fmt.Println("Error loading metadata:", err)
panic(err)
}
// Initialize cache with loaded metadata
cache, err := remotefetcher.NewCache(metadataFile, cacheDir)
if err != nil {
fmt.Println("Error initializing cache:", err)
panic(err)
}
// Populate cache with loaded metadata
for _, data := range opts.CachedData {
cache.AddDataToStore(data.Hash, *data)
}
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.NewConfigFetcher(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 != "" { if opts.ConfigFilePath != "" {
err := testFile(opts.ConfigFilePath) loadConfigFile(fetcher, opts.ConfigFilePath, backyKoanf, opts)
if err != nil {
logging.ExitWithMSG(fmt.Sprintf("Could not open config file %s: %v", opts.ConfigFilePath, err), 1, nil)
}
if err := backyKoanf.Load(file.Provider(opts.ConfigFilePath), yaml.Parser()); err != nil {
logging.ExitWithMSG(fmt.Sprintf("error loading config: %v", err), 1, &opts.Logger)
}
} 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.ConfigFetcher, 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.ConfigFetcher, configFiles []string, k *koanf.Koanf, opts *ConfigOpts) {
cFileFailures := 0
for _, c := range configFiles {
data, err := fetcher.Fetch(c)
if err != nil {
cFileFailures++
continue
}
if err := k.Load(rawbytes.Provider(data), yaml.Parser()); err != nil {
cFileFailures++
continue
}
break
}
if cFileFailures == len(configFiles) {
logging.ExitWithMSG("Could not find any valid config file", 1, nil)
}
}
func (opts *ConfigOpts) ReadConfig() *ConfigOpts {
setTerminalEnv()
backyKoanf := opts.koanf backyKoanf := opts.koanf
opts.loadEnv() opts.loadEnv()
@ -94,228 +138,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 +182,213 @@ 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 %s 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)
listConfigFiles = []string{u.JoinPath("lists.yml").String(), u.JoinPath("lists.yaml").String()}
} else {
backyConfigFileDir = path.Dir(opts.ConfigFilePath)
listConfigFiles = []string{
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("cmd-lists") {
unmarshalConfig(backyKoanf, "cmd-lists", &opts.CmdConfigLists, opts.Logger)
if backyKoanf.Exists("cmd-lists.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.NewConfigFetcher(filePath, opts.Cache, remotefetcher.IgnoreFileNotFound())
if err != nil {
// if file not found, ignore
if errors.Is(err, remotefetcher.ErrFileNotFound) {
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
}
opts.CmdListFile = filePath
return true
}
func loadCmdListsFile(backyKoanf *koanf.Koanf, listsConfig *koanf.Koanf, opts *ConfigOpts) {
opts.CmdListFile = strings.TrimSpace(backyKoanf.String("cmd-lists.file"))
if !path.IsAbs(opts.CmdListFile) {
opts.CmdListFile = path.Join(path.Dir(opts.ConfigFilePath), opts.CmdListFile)
}
fetcher, err := remotefetcher.NewConfigFetcher(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, "cmd-lists", &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 opts.cronEnabled && strings.TrimSpace(cmdList.Cron) == "" {
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)
} }
@ -448,6 +510,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 +566,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 +574,33 @@ 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)
}
} }
} }
@ -557,3 +647,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

@ -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,51 @@ 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)
logger.Info().Str("cmd", commandName).Str("output", line).Send() if wantOutput {
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
}

View File

@ -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"`
@ -114,20 +119,32 @@ type (
// 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 // UserGroups specifies the groups to add the user to
Groups []string `yaml:"groups,omitempty"` UserGroups []string `yaml:"userGroups,omitempty"`
// Home specifies the home directory for the user // UserHome specifies the home directory for the user
Home string `yaml:"home,omitempty"` UserHome string `yaml:"userHome,omitempty"`
// System specifies whether the user is a system account // UserShell specifies the shell for the user
System bool `yaml:"system,omitempty"` UserShell string `yaml:"userShell,omitempty"`
// Password specifies the password for the user (can be file: or plain text) // SystemUser specifies whether the user is a system account
Password string `yaml:"password,omitempty"` SystemUser bool `yaml:"systemUser,omitempty"`
// Operation specifies the action for user-related commands (e.g., "create" or "remove") // UserPassword specifies the password for the user (can be file: or plain text)
Operation string `yaml:"operation,omitempty"` 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 *strings.Reader
} }
RemoteSource struct { RemoteSource struct {
@ -176,6 +193,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 +218,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 +275,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"
@ -56,6 +57,13 @@ func SetCmdsToSearch(cmds []string) BackyOptionFunc {
} }
} }
// SetLogFile sets the path to the log file
func SetLogFile(logFile string) BackyOptionFunc {
return func(bco *ConfigOpts) {
bco.LogFilePath = logFile
}
}
// cronEnabled enables the execution of command lists at specified times // cronEnabled enables the execution of command lists at specified times
func CronEnabled() BackyOptionFunc { func CronEnabled() BackyOptionFunc {
return func(bco *ConfigOpts) { return func(bco *ConfigOpts) {
@ -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.
@ -37,6 +40,7 @@ func (a *AptManager) Install(pkg, version string, args []string) (string, []stri
if args != nil { if args != nil {
baseArgs = append(baseArgs, args...) baseArgs = append(baseArgs, args...)
} }
fmt.Printf("baseArgs: %v\n", baseArgs)
return baseCmd, baseArgs return baseCmd, baseArgs
} }
@ -53,7 +57,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 +66,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 +105,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 {

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

@ -0,0 +1,191 @@
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 {
// println("Saving cache to file:", c.file)
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()
println("Getting cache data for hash:", hash)
cacheData, exists := c.store[hash]
if !exists {
println("Cache data does not exist for hash:", hash)
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) {
c.mu.Lock()
defer c.mu.Unlock()
c.store[hash] = cacheData
}
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 hashMetadataSample.yml file
func LoadMetadataFromFile(filePath string) ([]*CacheData, error) {
// fmt.Println("Loading metadata from file:", filePath)
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,75 @@
package remotefetcher
import (
"errors"
"strings"
)
type ConfigFetcher 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
}
// ErrFileNotFound is returned when the file is not found and should be ignored
var ErrFileNotFound = errors.New("remotefetcher: file not found")
func NewConfigFetcher(source string, cache *Cache, options ...Option) (ConfigFetcher, error) {
var fetcher ConfigFetcher
var dataType string
config := FetcherConfig{}
for _, option := range options {
option(&config)
}
if strings.HasPrefix(source, "http") || strings.HasPrefix(source, "https") {
fetcher = NewHTTPFetcher(options...)
dataType = "yaml"
} else if strings.HasPrefix(source, "s3") {
var err error
fetcher, err = NewS3Fetcher(options...)
if err != nil {
return nil, err
}
dataType = "yaml"
} else {
fetcher = &LocalFetcher{}
dataType = "yaml"
return fetcher, nil
}
//TODO: should local files be cached?
data, err := fetcher.Fetch(source)
if err != nil {
if config.IgnoreFileNotFound && isFileNotFoundError(err) {
return nil, ErrFileNotFound
}
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, dataType)
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 ...Option) *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, ErrFileNotFound
}
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, ErrFileNotFound
}
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,37 @@
package remotefetcher
import (
"net/http"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
// Option is a function that configures a fetcher.
type Option func(*FetcherConfig)
// FetcherConfig holds the configuration for a fetcher.
type FetcherConfig struct {
S3Client *s3.Client
HTTPClient *http.Client
IgnoreFileNotFound bool
}
// WithS3Client sets the S3 client for the fetcher.
func WithS3Client(client *s3.Client) Option {
return func(cfg *FetcherConfig) {
cfg.S3Client = client
}
}
// WithHTTPClient sets the HTTP client for the fetcher.
func WithHTTPClient(client *http.Client) Option {
return func(cfg *FetcherConfig) {
cfg.HTTPClient = client
}
}
func IgnoreFileNotFound() Option {
return func(cfg *FetcherConfig) {
cfg.IgnoreFileNotFound = true
}
}

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

@ -0,0 +1,88 @@
package remotefetcher
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"strings"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"gopkg.in/yaml.v3"
)
type S3Fetcher struct {
S3Client *s3.Client
config FetcherConfig
}
// NewS3Fetcher creates a new instance of S3Fetcher with the provided options.
func NewS3Fetcher(options ...Option) (*S3Fetcher, error) {
cfg := &FetcherConfig{}
for _, opt := range options {
opt(cfg)
}
// Initialize S3 client if not provided
if cfg.S3Client == nil {
awsCfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
return nil, err
}
cfg.S3Client = s3.NewFromConfig(awsCfg)
}
return &S3Fetcher{S3Client: cfg.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, key, err := parseS3Source(source)
if err != nil {
return nil, err
}
resp, err := s.S3Client.GetObject(context.TODO(), &s3.GetObjectInput{
Bucket: &bucket,
Key: &key,
})
if err != nil {
var notFound *types.NoSuchKey
if errors.As(err, &notFound) && s.config.IgnoreFileNotFound {
return nil, ErrFileNotFound
}
return nil, err
}
defer resp.Body.Close()
buf := new(bytes.Buffer)
_, err = buf.ReadFrom(resp.Body)
if err != nil {
return nil, err
}
return buf.Bytes(), 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")
}
return parts[0], parts[1], nil
}
func (s *S3Fetcher) Hash(data []byte) string {
hash := sha256.Sum256(data)
return hex.EncodeToString(hash[:])
}

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