Compare commits
	
		
			44 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| e57939f858 | |||
| d45b1562fc | |||
| 7fe07f86a9 | |||
| 14c81a00a7 | |||
| 3eced64453 | |||
| c284d928fd | |||
| dd9da9452b | |||
| 11d5121954 | |||
| 66d622b474 | |||
| 47b2aabd9f | |||
| b91cf18b04 | |||
| 305b504ca1 | |||
| 7be2679b91 | |||
| 3c6e3ed914 | |||
| 02bc040e2a | |||
| 9f1f36215a | |||
| ff75f4bbcd | |||
| 5f40713e98 | |||
| cd5f7611a9 | |||
| b542711078 | |||
| 52dbc353e5 | |||
| 6bef0c3e5b | |||
| 4d705d78fb | |||
| 62d47ecfa7 | |||
| 32444ff82e | |||
| a5a7c05640 | |||
| bfb81e11b2 | |||
| fd4c83f9c0 | |||
| fe27c6396a | |||
| c89dde186a | |||
| 18a64de0de | |||
| 99c622b69f | |||
| 95e85e8b45 | |||
| 1a48c7bca5 | |||
| 5d21764ef1 | |||
| c7302f0e17 | |||
| fb1c8ec4fb | |||
| fe9462dac0 | |||
| d8453d1fb0 | |||
| 65c46a1e26 | |||
| f859b5961f | |||
| 25ddd65f25 | |||
| bcba6b2086 | |||
| 753b03861f | 
							
								
								
									
										3
									
								
								.changes/unreleased/Added-20250409-174528.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/unreleased/Added-20250409-174528.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | kind: Added | ||||||
|  | body: 'feat: Package operation `versionCheck` supports regular expressions (see [regexp](https://pkg.go.dev/regexp) package for docs)' | ||||||
|  | time: 2025-04-09T17:45:28.836497149-05:00 | ||||||
							
								
								
									
										3
									
								
								.changes/unreleased/Added-20250501-110745.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/unreleased/Added-20250501-110745.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | kind: Added | ||||||
|  | body: 'Command lists: added `cmdLists.[name].notify` object' | ||||||
|  | time: 2025-05-01T11:07:45.96164753-05:00 | ||||||
							
								
								
									
										3
									
								
								.changes/unreleased/Added-20250704-085917.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/unreleased/Added-20250704-085917.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | kind: Added | ||||||
|  | body: Testing setup with Docker | ||||||
|  | time: 2025-07-04T08:59:17.430373451-05:00 | ||||||
							
								
								
									
										3
									
								
								.changes/unreleased/Added-20250704-102126.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/unreleased/Added-20250704-102126.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | kind: Added | ||||||
|  | body: 'CLI: add global flag --hostsConfig that allows hosts to be dynamic in relation to the main config' | ||||||
|  | time: 2025-07-04T10:21:26.864635558-05:00 | ||||||
							
								
								
									
										3
									
								
								.changes/unreleased/Added-20250715-202303.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/unreleased/Added-20250715-202303.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | kind: Added | ||||||
|  | body: 'CLI: Exec subcommand `hosts`. See documentation for more details.' | ||||||
|  | time: 2025-07-15T20:23:03.647128713-05:00 | ||||||
							
								
								
									
										3
									
								
								.changes/unreleased/Added-20250723-220340.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/unreleased/Added-20250723-220340.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | kind: Added | ||||||
|  | body: 'CLI: added `exec hosts` subcommand `list`' | ||||||
|  | time: 2025-07-23T22:03:40.24191927-05:00 | ||||||
							
								
								
									
										3
									
								
								.changes/unreleased/Changed-20250321-090849.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/unreleased/Changed-20250321-090849.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | kind: Changed | ||||||
|  | body: 'Commands: `host` can now be `localhost` or `127.0.0.1` to run commands locally' | ||||||
|  | time: 2025-03-21T09:08:49.871021144-05:00 | ||||||
							
								
								
									
										3
									
								
								.changes/unreleased/Changed-20250325-003357.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/unreleased/Changed-20250325-003357.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | kind: Changed | ||||||
|  | body: lists loaded from external files only if no list config present in current file | ||||||
|  | time: 2025-03-25T00:33:57.039431409-05:00 | ||||||
							
								
								
									
										3
									
								
								.changes/unreleased/Changed-20250407-223020.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/unreleased/Changed-20250407-223020.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | kind: Changed | ||||||
|  | body: "`PackageManager.Parse` renamed to `ParseRemotePackageManagerVersionOutput`. This now returns arrays of PackageManagerCommon.Package and errors." | ||||||
|  | time: 2025-04-07T22:30:20.342177323-05:00 | ||||||
							
								
								
									
										3
									
								
								.changes/unreleased/Changed-20250418-133440.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/unreleased/Changed-20250418-133440.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | kind: Changed | ||||||
|  | body: 'Internal: refactoring and renaming functions' | ||||||
|  | time: 2025-04-18T13:34:40.842541658-05:00 | ||||||
							
								
								
									
										3
									
								
								.changes/unreleased/Changed-20250501-110534.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/unreleased/Changed-20250501-110534.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | kind: Changed | ||||||
|  | body: 'Commands: moved output-prefixed keys to the `commands.[name].output` object' | ||||||
|  | time: 2025-05-01T11:05:34.90130087-05:00 | ||||||
							
								
								
									
										3
									
								
								.changes/unreleased/Changed-20250609-072601.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/unreleased/Changed-20250609-072601.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | kind: Changed | ||||||
|  | body: Change internal method name for better understanding | ||||||
|  | time: 2025-06-09T07:26:01.819927627-05:00 | ||||||
							
								
								
									
										3
									
								
								.changes/unreleased/Changed-20250709-231919.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/unreleased/Changed-20250709-231919.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | kind: Changed | ||||||
|  | body: Improved error message for remote version package output | ||||||
|  | time: 2025-07-09T23:19:19.431960446-05:00 | ||||||
							
								
								
									
										3
									
								
								.changes/unreleased/Fixed-20250418-095747.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/unreleased/Fixed-20250418-095747.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | kind: Fixed | ||||||
|  | body: 'Command Lists: hooks now run correctly when commands finish' | ||||||
|  | time: 2025-04-18T09:57:47.39035092-05:00 | ||||||
							
								
								
									
										3
									
								
								.changes/unreleased/Fixed-20250424-225711.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/unreleased/Fixed-20250424-225711.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | kind: Fixed | ||||||
|  | body: Log file passed using `--log-file` correctly used | ||||||
|  | time: 2025-04-24T22:57:11.592829277-05:00 | ||||||
							
								
								
									
										16
									
								
								.changes/v0.10.0.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								.changes/v0.10.0.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | ## v0.10.0 - 2025-03-08 | ||||||
|  | ### Added | ||||||
|  | * Hooks: improved logging when executing | ||||||
|  | * User commands: adding SSH keys using config key `userSshPubKeys` | ||||||
|  | * directives: added support for fetching values using directive `%{externalSource:key}%` | ||||||
|  | ### Changed | ||||||
|  | * Commands: if dir is not specified, run in config dir | ||||||
|  | * FileDirective: use the config directory if path is not absolute | ||||||
|  | * Host: changes to case of some keys | ||||||
|  | * Notifications: added external directive to sensitive keys | ||||||
|  | ### Fixed | ||||||
|  | * LocalFetcher: return fetch error | ||||||
|  | * Lists: load file key before attempting to load from current file | ||||||
|  | * fix: host not in config file, but in ssh config, properly added to hosts struct | ||||||
|  | * SSH: password authentication bugs | ||||||
|  | * User commands: change user password works | ||||||
							
								
								
									
										8
									
								
								.changes/v0.10.1.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.changes/v0.10.1.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | ## v0.10.1 - 2025-03-11 | ||||||
|  | ### Added | ||||||
|  | * UserCommands: add ssh public keys when running locally | ||||||
|  | * UserCommands: add field CreateUserHome | ||||||
|  | ### Changed | ||||||
|  | * UserCommands: create temp file when modifing password over SSH | ||||||
|  | * UserCommands: change field name | ||||||
|  | * Vault: keys are now referenced by `name`, and the actual data by `data` | ||||||
							
								
								
									
										6
									
								
								.changes/v0.10.2.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/v0.10.2.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | ## v0.10.2 - 2025-03-19 | ||||||
|  | ### Added | ||||||
|  | * Notifications: http service added | ||||||
|  | * Variable support. Can be referenced with `%{var:nameOfVar}%` in select string fields. | ||||||
|  | ### Changed | ||||||
|  | * vault: initialize vault before validating config | ||||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -5,6 +5,6 @@ dist/ | |||||||
| .codegpt | .codegpt | ||||||
|  |  | ||||||
| *.log | *.log | ||||||
| *.sh | /*.sh | ||||||
| /*.yaml | /*.yaml | ||||||
| /*.yml | /*.yml | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							| @@ -9,5 +9,6 @@ | |||||||
|         "mautrix", |         "mautrix", | ||||||
|         "nikoksr", |         "nikoksr", | ||||||
|         "Strs" |         "Strs" | ||||||
|     ] |     ], | ||||||
|  |     "CodeGPT.apiKey": "CodeGPT Plus Beta" | ||||||
| } | } | ||||||
| @@ -1,9 +1,7 @@ | |||||||
| name: goreleaser release |  | ||||||
| steps: | steps: | ||||||
|   golang: |   golang: | ||||||
|     image: golang:1.23 |     image: golang:1.23 | ||||||
|     commands: |     commands: | ||||||
|       - go mod tidy |  | ||||||
|       - go install github.com/goreleaser/goreleaser/v2@v2.7.0 |       - go install github.com/goreleaser/goreleaser/v2@v2.7.0 | ||||||
|       - goreleaser release -f .goreleaser/gitea.yml --release-notes=".changes/$(go run backy.go version -V).md" |       - goreleaser release -f .goreleaser/gitea.yml --release-notes=".changes/$(go run backy.go version -V).md" | ||||||
|     environment: |     environment: | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ steps: | |||||||
|       - go build |       - go build | ||||||
|       - go test |       - go test | ||||||
|   release: |   release: | ||||||
|     image: golangci/golangci-lint:v1.53.3 |     image: golangci/golangci-lint:v1.64.7 | ||||||
|     commands: |     commands: | ||||||
|       - golangci-lint run -v --timeout 5m |       - golangci-lint run -v --timeout 5m | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										33
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -6,6 +6,39 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), | |||||||
| and is generated by [Changie](https://github.com/miniscruff/changie). | and is generated by [Changie](https://github.com/miniscruff/changie). | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## v0.10.2 - 2025-03-19 | ||||||
|  | ### Added | ||||||
|  | * Notifications: http service added | ||||||
|  | * Variable support. Can be referenced with `%{var:nameOfVar}%` in select string fields. | ||||||
|  | ### Changed | ||||||
|  | * vault: initialize vault before validating config | ||||||
|  |  | ||||||
|  | ## v0.10.1 - 2025-03-11 | ||||||
|  | ### Added | ||||||
|  | * UserCommands: add ssh public keys when running locally | ||||||
|  | * UserCommands: add field CreateUserHome | ||||||
|  | ### Changed | ||||||
|  | * UserCommands: create temp file when modifing password over SSH | ||||||
|  | * UserCommands: change field name | ||||||
|  | * Vault: keys are now referenced by `name`, and the actual data by `data` | ||||||
|  |  | ||||||
|  | ## v0.10.0 - 2025-03-08 | ||||||
|  | ### Added | ||||||
|  | * Hooks: improved logging when executing | ||||||
|  | * User commands: adding SSH keys using config key `userSshPubKeys` | ||||||
|  | * directives: added support for fetching values using directive `%{externalSource:key}%` | ||||||
|  | ### Changed | ||||||
|  | * Commands: if dir is not specified, run in config dir | ||||||
|  | * FileDirective: use the config directory if path is not absolute | ||||||
|  | * Host: changes to case of some keys | ||||||
|  | * Notifications: added external directive to sensitive keys | ||||||
|  | ### Fixed | ||||||
|  | * LocalFetcher: return fetch error | ||||||
|  | * Lists: load file key before attempting to load from current file | ||||||
|  | * fix: host not in config file, but in ssh config, properly added to hosts struct | ||||||
|  | * SSH: password authentication bugs | ||||||
|  | * User commands: change user password works | ||||||
|  |  | ||||||
| ## v0.9.1 - 2025-03-01 | ## v0.9.1 - 2025-03-01 | ||||||
| ### Changed | ### Changed | ||||||
| * Use EnvVar AWS_PROFILE to get S3 profile | * Use EnvVar AWS_PROFILE to get S3 profile | ||||||
|   | |||||||
| @@ -18,6 +18,8 @@ | |||||||
| 			"maunium", | 			"maunium", | ||||||
| 			"mautrix", | 			"mautrix", | ||||||
| 			"nikoksr", | 			"nikoksr", | ||||||
|  | 			"packagemanagercommon", | ||||||
|  | 			"rawbytes", | ||||||
| 			"remotefetcher", | 			"remotefetcher", | ||||||
| 			"Strs" | 			"Strs" | ||||||
| 		] | 		] | ||||||
|   | |||||||
| @@ -30,9 +30,14 @@ func init() { | |||||||
| } | } | ||||||
|  |  | ||||||
| func Backup(cmd *cobra.Command, args []string) { | func Backup(cmd *cobra.Command, args []string) { | ||||||
| 	backyConfOpts := backy.NewOpts(cfgFile, backy.AddCommandLists(cmdLists), backy.SetLogFile(logFile), backy.SetCmdStdOut(cmdStdOut)) | 	backyConfOpts := backy.NewConfigOptions(configFile, | ||||||
|  | 		backy.AddCommandLists(cmdLists), | ||||||
|  | 		backy.SetLogFile(logFile), | ||||||
|  | 		backy.EnableCommandStdOut(cmdStdOut), | ||||||
|  | 		backy.SetHostsConfigFile(hostsConfigFile)) | ||||||
|  |  | ||||||
| 	backyConfOpts.InitConfig() | 	backyConfOpts.InitConfig() | ||||||
| 	backyConfOpts.ReadConfig() | 	backyConfOpts.ParseConfigurationFile() | ||||||
|  |  | ||||||
| 	backyConfOpts.RunListConfig("") | 	backyConfOpts.RunListConfig("") | ||||||
| 	for _, host := range backyConfOpts.Hosts { | 	for _, host := range backyConfOpts.Hosts { | ||||||
|   | |||||||
							
								
								
									
										54
									
								
								cmd/backup_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								cmd/backup_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | |||||||
|  | package cmd | ||||||
|  |  | ||||||
|  | // import ( | ||||||
|  | // 	"bufio" | ||||||
|  | // 	"encoding/json" | ||||||
|  | // 	"os" | ||||||
|  | // 	"os/exec" | ||||||
|  | // 	"strings" | ||||||
|  | // 	"testing" | ||||||
|  |  | ||||||
|  | // 	"github.com/stretchr/testify/assert" | ||||||
|  | // ) | ||||||
|  |  | ||||||
|  | // // TestConfigOptions tests the configuration options for the backy package. | ||||||
|  | // func Test_ErrorHook(t *testing.T) { | ||||||
|  | // 	configFile := "-f ../../tests/ErrorHook.yml" | ||||||
|  | // 	logFile := "--log-file=ErrorHook.log" | ||||||
|  | // 	backyCommand := exec.Command("go", "run", "../../backy.go", configFile, logFile, "backup") | ||||||
|  | // 	backyCommand.Stderr = os.Stdout | ||||||
|  | // 	backyCommand.Stdout = os.Stdout | ||||||
|  | // 	err := backyCommand.Run() | ||||||
|  | // 	assert.Nil(t, err) | ||||||
|  | // 	os.Remove("ErrorHook.log") | ||||||
|  | // 	logFileData, logFileErr := os.ReadFile("ErrorHook.log") | ||||||
|  | // 	if logFileErr != nil { | ||||||
|  | // 		assert.FailNow(t, logFileErr.Error()) | ||||||
|  |  | ||||||
|  | // 	} | ||||||
|  | // 	var JsonData []map[string]interface{} | ||||||
|  | // 	jsonScanner := bufio.NewScanner(strings.NewReader(string(logFileData))) | ||||||
|  |  | ||||||
|  | // 	for jsonScanner.Scan() { | ||||||
|  | // 		var jsonDataLine map[string]interface{} | ||||||
|  | // 		err = json.Unmarshal(jsonScanner.Bytes(), &jsonDataLine) | ||||||
|  | // 		assert.Nil(t, err) | ||||||
|  | // 		JsonData = append(JsonData, jsonDataLine) | ||||||
|  | // 	} | ||||||
|  | // 	for _, v := range JsonData { | ||||||
|  | // 		_, ok := v["error"] | ||||||
|  | // 		if !ok { | ||||||
|  | // 			assert.FailNow(t, "error does not exist\n") | ||||||
|  | // 			// return | ||||||
|  | // 		} | ||||||
|  | // 	} | ||||||
|  | // 	// t.Logf("%s", logFileData) | ||||||
|  | // 	// t.Logf("%v", JsonData) | ||||||
|  | // } | ||||||
|  |  | ||||||
|  | // func TestBackupErrorHook(t *testing.T) { | ||||||
|  | // 	logFile = "ErrorHook.log" | ||||||
|  |  | ||||||
|  | // 	configFile = "../tests/ErrorHook.yml" | ||||||
|  |  | ||||||
|  | // } | ||||||
| @@ -20,7 +20,7 @@ package cmd | |||||||
|  |  | ||||||
| // func config(cmd *cobra.Command, args []string) { | // func config(cmd *cobra.Command, args []string) { | ||||||
|  |  | ||||||
| // 	opts := backy.NewOpts(cfgFile, backy.cronEnabled()) | // 	opts := backy.NewConfigOptions(configFile, backy.cronEnabled()) | ||||||
| // 	opts.InitConfig() | // 	opts.InitConfig() | ||||||
|  |  | ||||||
| // } | // } | ||||||
|   | |||||||
| @@ -18,13 +18,14 @@ var ( | |||||||
| func cron(cmd *cobra.Command, args []string) { | func cron(cmd *cobra.Command, args []string) { | ||||||
| 	parseS3Config() | 	parseS3Config() | ||||||
|  |  | ||||||
| 	opts := backy.NewOpts(cfgFile, | 	opts := backy.NewConfigOptions(configFile, | ||||||
| 		backy.EnableCron(), | 		backy.EnableCron(), | ||||||
| 		backy.SetLogFile(logFile), | 		backy.SetLogFile(logFile), | ||||||
| 		backy.SetCmdStdOut(cmdStdOut)) | 		backy.EnableCommandStdOut(cmdStdOut), | ||||||
|  | 		backy.SetHostsConfigFile(hostsConfigFile)) | ||||||
|  |  | ||||||
| 	opts.InitConfig() | 	opts.InitConfig() | ||||||
| 	opts.ReadConfig() | 	opts.ParseConfigurationFile() | ||||||
|  |  | ||||||
| 	opts.Cron() | 	opts.Cron() | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								cmd/exec.go
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								cmd/exec.go
									
									
									
									
									
								
							| @@ -21,7 +21,7 @@ var ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| func init() { | func init() { | ||||||
| 	execCmd.AddCommand(hostExecCommand) | 	execCmd.AddCommand(hostExecCommand, hostsExecCommand) | ||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -32,8 +32,12 @@ 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), backy.SetLogFile(logFile), backy.SetCmdStdOut(cmdStdOut)) | 	opts := backy.NewConfigOptions(configFile, | ||||||
|  | 		backy.AddCommands(args), | ||||||
|  | 		backy.SetLogFile(logFile), | ||||||
|  | 		backy.EnableCommandStdOut(cmdStdOut), | ||||||
|  | 		backy.SetHostsConfigFile(hostsConfigFile)) | ||||||
| 	opts.InitConfig() | 	opts.InitConfig() | ||||||
| 	opts.ReadConfig() | 	opts.ParseConfigurationFile() | ||||||
| 	opts.ExecuteCmds() | 	opts.ExecuteCmds() | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										17
									
								
								cmd/host.go
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								cmd/host.go
									
									
									
									
									
								
							| @@ -35,10 +35,13 @@ 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, backy.SetLogFile(logFile), backy.SetCmdStdOut(cmdStdOut)) | 	backyConfOpts := backy.NewConfigOptions(configFile, | ||||||
|  | 		backy.SetLogFile(logFile), | ||||||
|  | 		backy.EnableCommandStdOut(cmdStdOut), | ||||||
|  | 		backy.SetHostsConfigFile(hostsConfigFile)) | ||||||
| 	backyConfOpts.InitConfig() | 	backyConfOpts.InitConfig() | ||||||
|  |  | ||||||
| 	backyConfOpts.ReadConfig() | 	backyConfOpts.ParseConfigurationFile() | ||||||
|  |  | ||||||
| 	// check CLI input | 	// check CLI input | ||||||
| 	if hostsList == nil { | 	if hostsList == nil { | ||||||
| @@ -46,14 +49,20 @@ func Host(cmd *cobra.Command, args []string) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, h := range hostsList { | 	for _, h := range hostsList { | ||||||
|  | 		if backy.IsHostLocal(h) { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
| 		// check if h exists in the config file | 		// check if h exists in the config file | ||||||
| 		_, hostFound := backyConfOpts.Hosts[h] | 		_, hostFound := backyConfOpts.Hosts[h] | ||||||
| 		if !hostFound { | 		if !hostFound { | ||||||
| 			// check if h exists in the SSH config file | 			// check if h exists in the SSH config file | ||||||
| 			hostFoundInConfig, s := backy.CheckIfHostHasHostName(h) | 			hostFoundInConfig, s := backy.DoesHostHaveHostName(h) | ||||||
| 			if !hostFoundInConfig { | 			if !hostFoundInConfig { | ||||||
| 				logging.ExitWithMSG("host "+h+" not found", 1, &backyConfOpts.Logger) | 				logging.ExitWithMSG("host "+h+" not found", 1, &backyConfOpts.Logger) | ||||||
| 			} | 			} | ||||||
|  | 			if backyConfOpts.Hosts == nil { | ||||||
|  | 				backyConfOpts.Hosts = make(map[string]*backy.Host) | ||||||
|  | 			} | ||||||
| 			// create host with hostname and host | 			// create host with hostname and host | ||||||
| 			backyConfOpts.Hosts[h] = &backy.Host{Host: h, HostName: s} | 			backyConfOpts.Hosts[h] = &backy.Host{Host: h, HostName: s} | ||||||
| 		} | 		} | ||||||
| @@ -68,5 +77,5 @@ func Host(cmd *cobra.Command, args []string) { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	backyConfOpts.ExecCmdsSSH(cmdList, hostsList) | 	backyConfOpts.ExecCmdsOnHosts(cmdList, hostsList) | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										93
									
								
								cmd/hosts.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								cmd/hosts.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | |||||||
|  | package cmd | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"maps" | ||||||
|  | 	"slices" | ||||||
|  |  | ||||||
|  | 	"git.andrewnw.xyz/CyberShell/backy/pkg/backy" | ||||||
|  | 	"git.andrewnw.xyz/CyberShell/backy/pkg/logging" | ||||||
|  | 	"github.com/spf13/cobra" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	runCommandsInParallel bool | ||||||
|  |  | ||||||
|  | 	hostsExecCommand = &cobra.Command{ | ||||||
|  | 		Use:   "hosts [--command=command1 --command=command2 ... | -c command1 -c command2 ...]", | ||||||
|  | 		Short: "Runs command defined in config file on the hosts in order specified.", | ||||||
|  | 		Long:  "Hosts executes specified commands on all the hosts defined in config file.\nUse the --commands or -c flag to choose the commands.", | ||||||
|  | 		Run:   Hosts, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	hostsListExecCommand = &cobra.Command{ | ||||||
|  | 		Use:   "list list1 list2 ...", | ||||||
|  | 		Short: "Runs lists in order specified defined in config file on all hosts.", | ||||||
|  | 		Long:  "Lists executes specified lists on all the hosts defined in hosts config.\nPass the names of lists as arguments after command.", | ||||||
|  | 		Run:   HostsList, | ||||||
|  | 	} | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	hostsExecCommand.AddCommand(hostsListExecCommand) | ||||||
|  | 	hostsListExecCommand.Flags().BoolVarP(&runCommandsInParallel, "parallel", "p", false, "Run commands in parallel on hosts") | ||||||
|  | 	parseS3Config() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // cli input should be hosts and commands. Hosts are defined in config files. | ||||||
|  | // commands can be passed by the following mutually exclusive options: | ||||||
|  | //    1. as a list of commands defined in the config file | ||||||
|  | //    2. stdin (on command line) (TODO) | ||||||
|  |  | ||||||
|  | func Hosts(cmd *cobra.Command, args []string) { | ||||||
|  | 	backyConfOpts := backy.NewConfigOptions(configFile, | ||||||
|  | 		backy.SetLogFile(logFile), | ||||||
|  | 		backy.EnableCommandStdOut(cmdStdOut), | ||||||
|  | 		backy.SetHostsConfigFile(hostsConfigFile)) | ||||||
|  | 	backyConfOpts.InitConfig() | ||||||
|  |  | ||||||
|  | 	backyConfOpts.ParseConfigurationFile() | ||||||
|  |  | ||||||
|  | 	for _, h := range backyConfOpts.Hosts { | ||||||
|  |  | ||||||
|  | 		hostsList = append(hostsList, h.Host) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if cmdList == nil { | ||||||
|  | 		logging.ExitWithMSG("error: commands must be specified", 1, &backyConfOpts.Logger) | ||||||
|  | 	} | ||||||
|  | 	for _, c := range cmdList { | ||||||
|  | 		_, cmdFound := backyConfOpts.Cmds[c] | ||||||
|  | 		if !cmdFound { | ||||||
|  | 			logging.ExitWithMSG("cmd "+c+" not found", 1, &backyConfOpts.Logger) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	backyConfOpts.ExecCmdsOnHosts(cmdList, hostsList) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func HostsList(cmd *cobra.Command, args []string) { | ||||||
|  | 	backyConfOpts := backy.NewConfigOptions(configFile, | ||||||
|  | 		backy.SetLogFile(logFile), | ||||||
|  | 		backy.EnableCommandStdOut(cmdStdOut), | ||||||
|  | 		backy.SetHostsConfigFile(hostsConfigFile)) | ||||||
|  | 	backyConfOpts.InitConfig() | ||||||
|  |  | ||||||
|  | 	backyConfOpts.ParseConfigurationFile() | ||||||
|  |  | ||||||
|  | 	if len(args) == 0 { | ||||||
|  | 		logging.ExitWithMSG("error: no lists specified", 1, &backyConfOpts.Logger) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, l := range args { | ||||||
|  | 		_, listFound := backyConfOpts.CmdConfigLists[l] | ||||||
|  | 		if !listFound { | ||||||
|  | 			logging.ExitWithMSG("list "+l+" not found", 1, &backyConfOpts.Logger) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	maps.DeleteFunc(backyConfOpts.CmdConfigLists, func(k string, v *backy.CmdList) bool { | ||||||
|  | 		return !slices.Contains(args, k) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	backyConfOpts.ExecuteListOnHosts(args, runCommandsInParallel) | ||||||
|  | } | ||||||
							
								
								
									
										20
									
								
								cmd/list.go
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								cmd/list.go
									
									
									
									
									
								
							| @@ -22,13 +22,13 @@ var ( | |||||||
| 		Use:   "cmds [cmd1 cmd2 cmd3...]", | 		Use:   "cmds [cmd1 cmd2 cmd3...]", | ||||||
| 		Short: "List commands defined in config file.", | 		Short: "List commands defined in config file.", | ||||||
| 		Long:  "List commands defined in config file", | 		Long:  "List commands defined in config file", | ||||||
| 		Run:   ListCmds, | 		Run:   ListCommands, | ||||||
| 	} | 	} | ||||||
| 	listCmdLists = &cobra.Command{ | 	listCmdLists = &cobra.Command{ | ||||||
| 		Use:   "lists [list1 list2 ...]", | 		Use:   "lists [list1 list2 ...]", | ||||||
| 		Short: "List lists defined in config file.", | 		Short: "List lists defined in config file.", | ||||||
| 		Long:  "List lists defined in config file", | 		Long:  "List lists defined in config file", | ||||||
| 		Run:   ListCmdLists, | 		Run:   ListCommandLists, | ||||||
| 	} | 	} | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -40,7 +40,7 @@ func init() { | |||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func ListCmds(cmd *cobra.Command, args []string) { | func ListCommands(cmd *cobra.Command, args []string) { | ||||||
|  |  | ||||||
| 	// setup based on whats passed in: | 	// setup based on whats passed in: | ||||||
| 	//   - cmds | 	//   - cmds | ||||||
| @@ -54,17 +54,19 @@ func ListCmds(cmd *cobra.Command, args []string) { | |||||||
|  |  | ||||||
| 	parseS3Config() | 	parseS3Config() | ||||||
|  |  | ||||||
| 	opts := backy.NewOpts(cfgFile, backy.SetLogFile(logFile)) | 	opts := backy.NewConfigOptions(configFile, | ||||||
|  | 		backy.SetLogFile(logFile), | ||||||
|  | 		backy.SetHostsConfigFile(hostsConfigFile)) | ||||||
|  |  | ||||||
| 	opts.InitConfig() | 	opts.InitConfig() | ||||||
| 	opts.ReadConfig() | 	opts.ParseConfigurationFile() | ||||||
|  |  | ||||||
| 	for _, v := range cmdsToList { | 	for _, v := range cmdsToList { | ||||||
| 		opts.ListCommand(v) | 		opts.ListCommand(v) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func ListCmdLists(cmd *cobra.Command, args []string) { | func ListCommandLists(cmd *cobra.Command, args []string) { | ||||||
|  |  | ||||||
| 	parseS3Config() | 	parseS3Config() | ||||||
|  |  | ||||||
| @@ -74,10 +76,12 @@ func ListCmdLists(cmd *cobra.Command, args []string) { | |||||||
| 		logging.ExitWithMSG("Error: lists subcommand needs lists", 1, nil) | 		logging.ExitWithMSG("Error: lists subcommand needs lists", 1, nil) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	opts := backy.NewOpts(cfgFile, backy.SetLogFile(logFile)) | 	opts := backy.NewConfigOptions(configFile, | ||||||
|  | 		backy.SetLogFile(logFile), | ||||||
|  | 		backy.SetHostsConfigFile(hostsConfigFile)) | ||||||
|  |  | ||||||
| 	opts.InitConfig() | 	opts.InitConfig() | ||||||
| 	opts.ReadConfig() | 	opts.ParseConfigurationFile() | ||||||
|  |  | ||||||
| 	for _, v := range listsToList { | 	for _, v := range listsToList { | ||||||
| 		opts.ListCommandList(v) | 		opts.ListCommandList(v) | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								cmd/root.go
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								cmd/root.go
									
									
									
									
									
								
							| @@ -13,7 +13,8 @@ import ( | |||||||
|  |  | ||||||
| var ( | var ( | ||||||
| 	// Used for flags. | 	// Used for flags. | ||||||
| 	cfgFile    string | 	configFile      string | ||||||
|  | 	hostsConfigFile string | ||||||
| 	verbose         bool | 	verbose         bool | ||||||
| 	cmdStdOut       bool | 	cmdStdOut       bool | ||||||
| 	logFile         string | 	logFile         string | ||||||
| @@ -35,12 +36,13 @@ func Execute() { | |||||||
| } | } | ||||||
|  |  | ||||||
| func init() { | func init() { | ||||||
| 	rootCmd.PersistentFlags().StringVar(&logFile, "log-file", "", "log file to write to") | 	rootCmd.PersistentFlags().StringVar(&logFile, "logFile", "", "log file to write to") | ||||||
| 	rootCmd.PersistentFlags().BoolVar(&cmdStdOut, "cmdStdOut", false, "Pass to print command output to stdout") | 	rootCmd.PersistentFlags().BoolVar(&cmdStdOut, "cmdStdOut", false, "Pass to print command output to stdout") | ||||||
|  |  | ||||||
| 	rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "f", "", "config file to read from") | 	rootCmd.PersistentFlags().StringVarP(&configFile, "config", "f", "", "config file to read from") | ||||||
|  | 	rootCmd.PersistentFlags().StringVar(&hostsConfigFile, "hostsConfig", "", "yaml hosts file to read from") | ||||||
| 	rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Sets verbose level") | 	rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Sets verbose level") | ||||||
| 	rootCmd.PersistentFlags().StringVar(&s3Endpoint, "s3-endpoint", "", "Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.") | 	rootCmd.PersistentFlags().StringVar(&s3Endpoint, "s3Endpoint", "", "Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.") | ||||||
| 	rootCmd.AddCommand(backupCmd, execCmd, cronCmd, versionCmd, listCmd) | 	rootCmd.AddCommand(backupCmd, execCmd, cronCmd, versionCmd, listCmd) | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ import ( | |||||||
| 	"github.com/spf13/cobra" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const versionStr = "0.9.1" | const versionStr = "0.10.2" | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| 	versionCmd = &cobra.Command{ | 	versionCmd = &cobra.Command{ | ||||||
|   | |||||||
| @@ -26,8 +26,9 @@ Flags: | |||||||
|       --cmdStdOut            Pass to print command output to stdout |       --cmdStdOut            Pass to print command output to stdout | ||||||
|   -f, --config string        config file to read from |   -f, --config string        config file to read from | ||||||
|   -h, --help                 help for backy |   -h, --help                 help for backy | ||||||
|       --log-file string      log file to write to |       --hostsConfig string   yaml hosts file to read from | ||||||
|       --s3-endpoint string   Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable. |       --logFile string       log file to write to | ||||||
|  |       --s3Endpoint string    Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable. | ||||||
|   -v, --verbose              Sets verbose level |   -v, --verbose              Sets verbose level | ||||||
|  |  | ||||||
| Use "backy [command] --help" for more information about a command. | Use "backy [command] --help" for more information about a command. | ||||||
| @@ -51,8 +52,9 @@ Flags: | |||||||
| Global Flags: | Global Flags: | ||||||
|       --cmdStdOut            Pass to print command output to stdout |       --cmdStdOut            Pass to print command output to stdout | ||||||
|   -f, --config string        config file to read from |   -f, --config string        config file to read from | ||||||
|       --log-file string      log file to write to |       --hostsConfig string   yaml hosts file to read from | ||||||
|       --s3-endpoint string   Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable. |       --logFile string       log file to write to | ||||||
|  |       --s3Endpoint string    Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable. | ||||||
|   -v, --verbose              Sets verbose level |   -v, --verbose              Sets verbose level | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| @@ -70,8 +72,9 @@ Flags: | |||||||
| Global Flags: | Global Flags: | ||||||
|       --cmdStdOut            Pass to print command output to stdout |       --cmdStdOut            Pass to print command output to stdout | ||||||
|   -f, --config string        config file to read from |   -f, --config string        config file to read from | ||||||
|       --log-file string      log file to write to |       --hostsConfig string   yaml hosts file to read from | ||||||
|       --s3-endpoint string   Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable. |       --logFile string       log file to write to | ||||||
|  |       --s3Endpoint string    Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable. | ||||||
|   -v, --verbose              Sets verbose level |   -v, --verbose              Sets verbose level | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| @@ -86,6 +89,7 @@ Usage: | |||||||
|  |  | ||||||
| Available Commands: | Available Commands: | ||||||
|   host        Runs command defined in config file on the hosts in order specified. |   host        Runs command defined in config file on the hosts in order specified. | ||||||
|  |   hosts       Runs command defined in config file on the hosts in order specified. | ||||||
|  |  | ||||||
| Flags: | Flags: | ||||||
|   -h, --help   help for exec |   -h, --help   help for exec | ||||||
| @@ -93,8 +97,9 @@ Flags: | |||||||
| Global Flags: | Global Flags: | ||||||
|       --cmdStdOut            Pass to print command output to stdout |       --cmdStdOut            Pass to print command output to stdout | ||||||
|   -f, --config string        config file to read from |   -f, --config string        config file to read from | ||||||
|       --log-file string      log file to write to |       --hostsConfig string   yaml hosts file to read from | ||||||
|       --s3-endpoint string   Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable. |       --logFile string       log file to write to | ||||||
|  |       --s3Endpoint string    Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable. | ||||||
|   -v, --verbose              Sets verbose level |   -v, --verbose              Sets verbose level | ||||||
|  |  | ||||||
| Use "backy exec [command] --help" for more information about a command. | Use "backy exec [command] --help" for more information about a command. | ||||||
| @@ -117,8 +122,9 @@ Flags: | |||||||
| Global Flags: | Global Flags: | ||||||
|       --cmdStdOut            Pass to print command output to stdout |       --cmdStdOut            Pass to print command output to stdout | ||||||
|   -f, --config string        config file to read from |   -f, --config string        config file to read from | ||||||
|       --log-file string      log file to write to |       --hostsConfig string   yaml hosts file to read from | ||||||
|       --s3-endpoint string   Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable. |       --logFile string       log file to write to | ||||||
|  |       --s3Endpoint string    Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable. | ||||||
|   -v, --verbose              Sets verbose level |   -v, --verbose              Sets verbose level | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| @@ -138,8 +144,9 @@ Flags: | |||||||
| Global Flags: | Global Flags: | ||||||
|       --cmdStdOut            Pass to print command output to stdout |       --cmdStdOut            Pass to print command output to stdout | ||||||
|   -f, --config string        config file to read from |   -f, --config string        config file to read from | ||||||
|       --log-file string      log file to write to |       --hostsConfig string   yaml hosts file to read from | ||||||
|       --s3-endpoint string   Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable. |       --logFile string       log file to write to | ||||||
|  |       --s3Endpoint string    Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable. | ||||||
|   -v, --verbose              Sets verbose level |   -v, --verbose              Sets verbose level | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| @@ -161,8 +168,9 @@ Flags: | |||||||
| Global Flags: | Global Flags: | ||||||
|       --cmdStdOut            Pass to print command output to stdout |       --cmdStdOut            Pass to print command output to stdout | ||||||
|   -f, --config string        config file to read from |   -f, --config string        config file to read from | ||||||
|       --log-file string      log file to write to |       --hostsConfig string   yaml hosts file to read from | ||||||
|       --s3-endpoint string   Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable. |       --logFile string       log file to write to | ||||||
|  |       --s3Endpoint string    Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable. | ||||||
|   -v, --verbose              Sets verbose level |   -v, --verbose              Sets verbose level | ||||||
|  |  | ||||||
| Use "backy list [command] --help" for more information about a command. | Use "backy list [command] --help" for more information about a command. | ||||||
| @@ -181,8 +189,9 @@ Flags: | |||||||
| Global Flags: | Global Flags: | ||||||
|       --cmdStdOut            Pass to print command output to stdout |       --cmdStdOut            Pass to print command output to stdout | ||||||
|   -f, --config string        config file to read from |   -f, --config string        config file to read from | ||||||
|       --log-file string      log file to write to |       --hostsConfig string   yaml hosts file to read from | ||||||
|       --s3-endpoint string   Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable. |       --logFile string       log file to write to | ||||||
|  |       --s3Endpoint string    Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable. | ||||||
|   -v, --verbose              Sets verbose level |   -v, --verbose              Sets verbose level | ||||||
| ``` | ``` | ||||||
| ## list lists | ## list lists | ||||||
| @@ -199,7 +208,8 @@ Flags: | |||||||
| Global Flags: | Global Flags: | ||||||
|       --cmdStdOut            Pass to print command output to stdout |       --cmdStdOut            Pass to print command output to stdout | ||||||
|   -f, --config string        config file to read from |   -f, --config string        config file to read from | ||||||
|       --log-file string      log file to write to |       --hostsConfig string   yaml hosts file to read from | ||||||
|       --s3-endpoint string   Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable. |       --logFile string       log file to write to | ||||||
|  |       --s3Endpoint string    Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable. | ||||||
|   -v, --verbose              Sets verbose level |   -v, --verbose              Sets verbose level | ||||||
| ``` | ``` | ||||||
|   | |||||||
| @@ -15,5 +15,5 @@ The `exec` subcommand can do some things that the configuration file can't do ye | |||||||
| The commands have to be defined in the config file. The hosts need to at least be in the ssh_config(5) file. | The commands have to be defined in the config file. The hosts need to at least be in the ssh_config(5) file. | ||||||
|  |  | ||||||
| ```sh | ```sh | ||||||
| backy exec host [--commands=command1 -commands=command2 ... | -c command1 -c command2 ...] [--hosts=host1 --hosts=hosts2 ... | -m host1 -c host2 ...]  [flags] | backy exec host [--commands=command1 -commands=command2 ... | -c command1 -c command2 ...] [--hosts=host1 --hosts=hosts2 ... | -m host1 -m host2 ...]  [flags] | ||||||
| ``` | ``` | ||||||
|   | |||||||
							
								
								
									
										29
									
								
								docs/content/cli/list.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								docs/content/cli/list.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | --- | ||||||
|  | title: List | ||||||
|  | --- | ||||||
|  |  | ||||||
|  |  | ||||||
|  | List commands, lists, or hosts defined in config file | ||||||
|  |  | ||||||
|  | Usage: | ||||||
|  | ``` | ||||||
|  |   backy list [command] | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Available Commands: | ||||||
|  |   cmds        List commands defined in config file. | ||||||
|  |   lists       List lists defined in config file. | ||||||
|  |  | ||||||
|  | Flags: | ||||||
|  | ``` | ||||||
|  |   -h, --help   help for list | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Global Flags: | ||||||
|  | ``` | ||||||
|  |       --cmdStdOut            Pass to print command output to stdout | ||||||
|  |   -f, --config string        config file to read from | ||||||
|  |       --log-file string      log file to write to | ||||||
|  |       --s3-endpoint string   Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable. | ||||||
|  |   -v, --verbose              Sets verbose level | ||||||
|  | ``` | ||||||
| @@ -35,12 +35,12 @@ If a remote config file is specified (on the command-line using `-f`) and the li | |||||||
| ``` | ``` | ||||||
|  |  | ||||||
| | key | description | type | required | | key | description | type | required | ||||||
| | --- | --- | --- | --- | | | --- | --- | --- | --- | ||||||
| | `order` | Defines the sequence of commands to execute | `[]string` | yes | | | `order` | Defines the sequence of commands to execute | `[]string` | yes | ||||||
| | `getOutput` | Command(s) output is in the notification(s) | `bool` | no | | | `sendNotificationOnSuccess` | Whether to send notification on list success with the commands' output | `bool` | no | ||||||
| | `notifications` | The notification service(s) and ID(s) to use on success and failure. Must be *`service.id`*. See the [notifications documentation page](/config/notifications/) for more | `[]string` | no | | | `notifications` | The notification service(s) and ID(s) to use on success and failure. Must be *`service.id`*. See the [notifications documentation page](/config/notifications/) for more | `[]string` | no | ||||||
| | `name` | Optional name of the list | `string` | no | | | `name` | Optional name of the list | `string` | no | ||||||
| | `cron` | Time at which to schedule the list. Only has affect when cron subcommand is run. | `string` | no | | | `cron` | Time at which to schedule the list. Only has affect when cron subcommand is run. | `string` | no | ||||||
|  |  | ||||||
| ### Order | ### Order | ||||||
|  |  | ||||||
|   | |||||||
| @@ -8,46 +8,21 @@ weight: 1 | |||||||
|  |  | ||||||
| ### Example Config | ### Example Config | ||||||
|  |  | ||||||
| ```yaml | {{% code file="/examples/example.yml" language="yaml" %}} | ||||||
| commands: |  | ||||||
|   stop-docker-container: |  | ||||||
|     cmd: docker |  | ||||||
|     Args: |  | ||||||
|       - compose |  | ||||||
|       - -f /some/path/to/docker-compose.yaml |  | ||||||
|       - down |  | ||||||
|     # if host is not defined, command will be run locally |  | ||||||
|     # The host has to be defined in either the config file or the SSH Config files |  | ||||||
|     host: some-host |  | ||||||
|     hooks |  | ||||||
|       error: |  | ||||||
|         - some-other-command-when-failing |  | ||||||
|       success: |  | ||||||
|         - success-command |  | ||||||
|       final: |  | ||||||
|         - final-command |  | ||||||
|   backup-docker-container-script: |  | ||||||
|     cmd: /path/to/local/script |  | ||||||
|     # script file is input as stdin to SSH |  | ||||||
|     type: scriptFile # also can be script |  | ||||||
|     environment: |  | ||||||
|       - FOO=BAR |  | ||||||
|       - APP=$VAR |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| Values available for this section **(case-sensitive)**: | Values available for this section **(case-sensitive)**: | ||||||
|  |  | ||||||
| | name | notes | type | required | | name            | notes                                                                                                   | type                  | required | External directive support | | ||||||
| | --- | --- | --- | --- | | | ----------------| ------------------------------------------------------------------------------------------------------- | --------------------- | -------- |----------------------------| | ||||||
| | `cmd` | Defines the command to execute | `string` | yes | | | `cmd`           | Defines the command to execute                                                                          | `string`              | yes      | No                         | | ||||||
| | `Args` | Defines the arguments to the command | `[]string` | no | | | `Args`          | Defines the arguments to the command                                                                    | `[]string`            | no       | No                         | | ||||||
| | `environment` | Defines environment variables for the command | `[]string` | no | | | `environment`   | Defines environment variables for the command                                                           | `[]string`            | no       | Partial                    | | ||||||
| | `type` | See documentation further down the page. Additional fields may be required. | `string` | no | | | `type`          | See documentation further down the page. Additional fields may be required.                             | `string`              | no       | No                         | | ||||||
| | `getOutput` | Command(s) output is in the notification(s) | `bool` | no | | | `getOutput`     | Command(s) output is in the notification(s)                                                             | `bool`                | no       | No                         | | ||||||
| | `host` | If not specified, the command will execute locally. | `string` | no | | | `host`          | If not specified, the command will execute locally.                                                     | `string`              | no       | No                         | | ||||||
| | `scriptEnvFile` | When type is `scriptFile` or `script`, this file is prepended to the input. | `string` | no | | | `scriptEnvFile` | When type is `scriptFile` or `script`, this file is prepended to the input.                             | `string`              | no       | No                         | | ||||||
| | `shell` | Run the command in the shell | `string` | no | | | `shell`         | Run the command in the shell                                                                            | `string`              | no       | No                         | | ||||||
| | `hooks` | Hooks are used at the end of the individual command. Must have at least `error`, `success`, or `final`. | `map[string][]string` | no | | | `hooks`         | Hooks are used at the end of the individual command. Must have at least `error`, `success`, or `final`. | `map[string][]string` | no       | No                         | | ||||||
|  |  | ||||||
| #### cmd | #### cmd | ||||||
|  |  | ||||||
| @@ -120,8 +95,9 @@ The following options are available: | |||||||
| The environment variables support expansion: | The environment variables support expansion: | ||||||
|  |  | ||||||
| - using escaped values `$VAR` or `${VAR}` | - using escaped values `$VAR` or `${VAR}` | ||||||
|  | - using any external directive, and if using the env directive, the variable will be read from a `.env` file | ||||||
|  |  | ||||||
| For now, the variables have to be defined in an `.env` file in the same directory that the program is run from. | <!-- For now, the variables expanded have to be defined in an `.env` file in the same directory that the program is run from. --> | ||||||
|  |  | ||||||
| If using it with host specified, the SSH server has to be configured to accept those env variables. | If using it with host specified, the SSH server has to be configured to accept those env variables. | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,14 +6,18 @@ description: This is dedicated to user commands. | |||||||
|  |  | ||||||
| This is dedicated to `user` commands. The command `type` field must be `user`. User is a type that allows one to perform user operations. There are several additional options available when `type` is `user`: | This is dedicated to `user` commands. The command `type` field must be `user`. User is a type that allows one to perform user operations. There are several additional options available when `type` is `user`: | ||||||
|  |  | ||||||
| | name | notes | type | required | | | name            | notes                                                        | type       | required | External directive support | ||||||
| | --- | --- | --- | --- | | | ----------------| -------------------------------------------------------------| ---------- | ---------| --------------------------| | ||||||
| | `userName` | The name of a user to be configured. | `string` | yes | | | `userName`      | The name of a user to be configured.                         | `string`   | yes      | no 						 | | ||||||
| | `userOperation` | The type of operation to perform. | `string` | yes | | | `userOperation` | The type of operation to perform.                            | `string`   | yes      | no 						 | | ||||||
| | `userID` | The user ID to use. | `string` | yes | | | `userID`        | The user ID to use.                                          | `string`   | no       | no 						 | | ||||||
| | `userGroups` | The groups the user should be added to. | `[]string` | yes | | | `userGroups`    | The groups the user should be added to.                      | `[]string` | no       | no 						 | | ||||||
| | `userShell` | The shell for the user. | `string` | yes | | | `systemUser`    | Create a system user.                                        | `bool`     | no       | no 						 | | ||||||
| | `userHome` | The user's home directory. | `string` | no | | | `userCreateHome`| Create the home directory.                                   | `bool`     | no       | no 						 | | ||||||
|  | | `userSshPubKeys`| The keys to add to the user's authorized keys.               | `[]string` | no       | yes 						 | | ||||||
|  | | `userShell`     | The shell for the user.                                      | `string`   | no       | no 						 | | ||||||
|  | | `userHome`      | The user's home directory.                                   | `string`   | no       | no 						 | | ||||||
|  | | `userPassword`  | The new password value when using the `password` operation.  | `string`   | no       | yes						 | | ||||||
|  |  | ||||||
|  |  | ||||||
| #### example | #### example | ||||||
|   | |||||||
							
								
								
									
										15
									
								
								docs/content/config/directives.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								docs/content/config/directives.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | --- | ||||||
|  | title: "External Directives" | ||||||
|  | weight: 2 | ||||||
|  | description: How to set up external directives. | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | External directives are for including data that should not be in the config file. The following directives are supported: | ||||||
|  |  | ||||||
|  | - `%{file:path/to/file}%` | ||||||
|  | - `%{env:ENV_VAR}%` | ||||||
|  | - `%{vault:vault-key}%` | ||||||
|  |  | ||||||
|  | See the docs of each command if the field is supported. | ||||||
|  |  | ||||||
|  | If the file path does not begin with a `/`, the config file's directory will be used as the starting point. | ||||||
| @@ -5,20 +5,20 @@ description: > | |||||||
|   This page tells you how to use hosts. |   This page tells you how to use hosts. | ||||||
| --- | --- | ||||||
|  |  | ||||||
| | Key                  | Description                                                   | Type     | Required | | | Key                  | Description                                                   | Type     | Required | External directive support | | ||||||
| |----------------------|---------------------------------------------------------------|----------|----------| | |----------------------|---------------------------------------------------------------|----------|----------|----------------------------| | ||||||
| | `OS`                 | Operating system of the host (used for package commands)      | `string` | no       | | | `OS`                 | Operating system of the host (used for package commands)      | `string` | no       | No                         | | ||||||
| | `config`             | Path to the SSH config file                                   | `string` | no       | | | `config`             | Path to the SSH config file                                   | `string` | no       | No                         | | ||||||
| | `host`               | Specifies the `Host` ssh_config(5) directive                  | `string` | yes      | | | `host`               | Specifies the `Host` ssh_config(5) directive                  | `string` | yes      | No                         | | ||||||
| | `hostname`           | Hostname of the host                                          | `string` | no       | | | `hostname`           | Hostname of the host                                          | `string` | no       | No                         | | ||||||
| | `knownhostsfile`     | Path to the known hosts file                                  | `string` | no       | | | `knownHostsFile`     | Path to the known hosts file                                  | `string` | no       | No                         | | ||||||
| | `port`               | Port number to connect to                                     | `uint16` | no       | | | `port`               | Port number to connect to                                     | `uint16` | no       | No                         | | ||||||
| | `proxyjump`          | Proxy jump hosts, comma-separated                             | `string` | no       | | | `proxyjump`          | Proxy jump hosts, comma-separated                             | `string` | no       | No                         | | ||||||
| | `password`           | Password for SSH authentication                               | `string` | no       | | | `password`           | Password for SSH authentication                               | `string` | no       | No                         | | ||||||
| | `privatekeypath`     | Path to the private key file                                  | `string` | no       | | | `privateKeyPath`     | Path to the private key file                                  | `string` | no       | No                         | | ||||||
| | `privatekeypassword` | Password for the private key file                             | `string` | no       | | | `privateKeyPassword` | Password for the private key file                             | `string` | no       | Yes                        | | ||||||
| | `user`               | Username for SSH authentication                               | `string` | no       | | | `user`               | Username for SSH authentication                               | `string` | no       | No                         | | ||||||
|  |  | ||||||
| ## exec host subcommand | ## exec host subcommand | ||||||
|  |  | ||||||
| Backy has a subcommand `exec host`. This subcommand takes the flags of `-m host1 -m host2`. For now these hosts need to be defined in the config file. | Backy has a subcommand `exec host`. This subcommand takes the flags of `-m host1 -m host2`. The commands can also be specified by `-c command1 -c command2`. | ||||||
|   | |||||||
| @@ -39,23 +39,23 @@ There must be a section with an id (eg. `mail.test-svr`) following one of these | |||||||
|  |  | ||||||
| ### mail | ### mail | ||||||
|  |  | ||||||
| | key | description | type | | key | description | type | External directive support | | ||||||
| | --- | --- | --- | | --- | --- | --- | --- | | ||||||
| | `host` | Specifies the SMTP host to connect to | `string` | | `host` | Specifies the SMTP host to connect to | `string` | no | ||||||
| | `port` | Specifies the SMTP port | `uint16` | | `port` | Specifies the SMTP port | `uint16` | no | ||||||
| | `senderaddress` | Address from which to send mail | `string` | | `senderaddress` | Address from which to send mail | `string` | no | ||||||
| | `to` | Recipients to send emails to | `[]string` | | `to` | Recipients to send emails to | `[]string` | no | ||||||
| | `username` | SMTP username | `string` | | `username` | SMTP username | `string` | no | ||||||
| | `password` | SMTP password | `string` | | `password` | SMTP password | `string` | yes | ||||||
|  |  | ||||||
| ### matrix | ### matrix | ||||||
|  |  | ||||||
| | key | description | type | | key | description | type | External directive support | | ||||||
| | --- | --- | --- | | --- | --- | ---| ---- | | ||||||
| | `home-server` | Specifies the Matrix server connect to | `string` | | `home-server` | Specifies the Matrix server connect to | `string` | no | ||||||
| | `room-id` | Specifies the room ID of the room to send messages to | `string` | | `room-id` | Specifies the room ID of the room to send messages to | `string` | no | ||||||
| | `access-token` | Matrix access token | `string` | | `access-token` | Matrix access token  | `string` | yes | ||||||
| | `user-id` | Matrix user ID | `string` | | `user-id` | Matrix user ID | `string` | no | ||||||
|  |  | ||||||
| To get your access token (assumes you are using [Element](https://element.io/)) : | To get your access token (assumes you are using [Element](https://element.io/)) : | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ 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. | ||||||
|  |  | ||||||
| Vault config can be used by prefixing `vault:` in front of a password or ENV var. | A Vault key can be used by prefixing `%{vault:vault.keys.name}%` in a field that supports external directives. | ||||||
|  |  | ||||||
| This is the object in the config file: | This is the object in the config file: | ||||||
|  |  | ||||||
| @@ -18,10 +18,12 @@ vault: | |||||||
|   keys: |   keys: | ||||||
|     - name: mongourl |     - name: mongourl | ||||||
|       mountpath: secret |       mountpath: secret | ||||||
|  |       key: data | ||||||
|       path: mongo/url |       path: mongo/url | ||||||
|       type:  # KVv1 or KVv2 |       type: KVv2  # KVv1 or KVv2 | ||||||
|     - name: |     - name: someKeyName | ||||||
|       path: |       mountpath: secret | ||||||
|       type: |       key: keyData | ||||||
|       mountpath: |       type: KVv2 | ||||||
|  |       path: some/path | ||||||
| ``` | ``` | ||||||
|   | |||||||
							
								
								
									
										108
									
								
								docs/content/examples/backy.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								docs/content/examples/backy.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | |||||||
|  | commands: | ||||||
|  |   stop-docker-container: | ||||||
|  |     cmd: docker | ||||||
|  |     Args: | ||||||
|  |       - compose | ||||||
|  |       - -f /some/path/to/docker-compose.yaml | ||||||
|  |       - down | ||||||
|  |     # if host is not defined, cmd will be run locally | ||||||
|  |     host: some-host | ||||||
|  |     hooks: | ||||||
|  |       final: | ||||||
|  |         - hostname | ||||||
|  |       error: | ||||||
|  |         - hostname | ||||||
|  |   backup-docker-container-script: | ||||||
|  |     cmd: /path/to/script | ||||||
|  |     # The host has to be defined in the config file | ||||||
|  |     host: some-host | ||||||
|  |     environment: | ||||||
|  |       - FOO=BAR | ||||||
|  |       - APP=$VAR | ||||||
|  |   shell-cmd: | ||||||
|  |     cmd: rsync | ||||||
|  |     shell: bash | ||||||
|  |     Args: | ||||||
|  |       - -av some-host:/path/to/data ~/Docker/Backups/docker-data | ||||||
|  |   hostname: | ||||||
|  |     cmd: hostname | ||||||
|  |   update-docker: | ||||||
|  |     type: package | ||||||
|  |     shell: zsh # best to run package commands in a shell | ||||||
|  |     packageName: docker-ce | ||||||
|  |     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: | ||||||
|  |   cmds-to-run: # this can be any name you want | ||||||
|  |     # all commands have to be defined | ||||||
|  |     order: | ||||||
|  |       - stop-docker-container | ||||||
|  |       - backup-docker-container-script | ||||||
|  |       - shell-cmd | ||||||
|  |       - hostname | ||||||
|  |     notifications: | ||||||
|  |       - matrix.matrix | ||||||
|  |     name: backup-some-server | ||||||
|  |     cron: "0 0 1 * * *" | ||||||
|  |   hostname: | ||||||
|  |     name: hostname | ||||||
|  |     order: | ||||||
|  |       - hostname | ||||||
|  |     notifications: | ||||||
|  |       - mail.prod-email | ||||||
|  |  | ||||||
|  | hosts: | ||||||
|  |   # any ssh_config(5) keys/values not listed here will be looked up in the config file or the default config file | ||||||
|  |   some-host: | ||||||
|  |     hostname: some-hostname | ||||||
|  |     config: ~/.ssh/config | ||||||
|  |     user: user | ||||||
|  |     privateKeyPath: /path/to/private/key | ||||||
|  |     port: 22 | ||||||
|  |     # can also be env:VAR | ||||||
|  |     password: file:/path/to/file | ||||||
|  |     # only one is supported for now | ||||||
|  |     proxyjump: some-proxy-host | ||||||
|  |  | ||||||
|  | # optional | ||||||
|  | logging: | ||||||
|  |   verbose: true | ||||||
|  |   file: ./backy.log | ||||||
|  |   console: false | ||||||
|  |   cmd-std-out: false | ||||||
|  |  | ||||||
|  |  | ||||||
|  | notifications: | ||||||
|  |   mail: | ||||||
|  |     prod-email: | ||||||
|  |       id: prod-email | ||||||
|  |       type: mail | ||||||
|  |       host: yourhost.tld | ||||||
|  |       port: 587 | ||||||
|  |       senderAddress: email@domain.tld | ||||||
|  |       to: | ||||||
|  |         - admin@domain.tld | ||||||
|  |       username: smtp-username@domain.tld | ||||||
|  |       password: your-password-here | ||||||
|  |   matrix: | ||||||
|  |     matrix: | ||||||
|  |       id: matrix | ||||||
|  |       type: matrix | ||||||
|  |       home-server: your-home-server.tld | ||||||
|  |       room-id: room-id | ||||||
|  |       access-token: your-access-token | ||||||
|  |       user-id: your-user-id | ||||||
							
								
								
									
										24
									
								
								docs/content/examples/example.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								docs/content/examples/example.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | commands: | ||||||
|  |   stop-docker-container: | ||||||
|  |     cmd: docker | ||||||
|  |     Args: | ||||||
|  |       - compose | ||||||
|  |       - -f /some/path/to/docker-compose.yaml | ||||||
|  |       - down | ||||||
|  |     # if host is not defined, command will be run locally | ||||||
|  |     # The host has to be defined in either the config file or the SSH Config files | ||||||
|  |     host: some-host | ||||||
|  |     hooks: | ||||||
|  |       error: | ||||||
|  |         - some-other-command-when-failing | ||||||
|  |       success: | ||||||
|  |         - success-command | ||||||
|  |       final: | ||||||
|  |         - final-command | ||||||
|  |   backup-docker-container-script: | ||||||
|  |     cmd: /path/to/local/script | ||||||
|  |     # script file is input as stdin to SSH | ||||||
|  |     type: scriptFile # also can be script | ||||||
|  |     environment: | ||||||
|  |       - FOO=BAR | ||||||
|  |       - APP=$VAR | ||||||
| @@ -124,13 +124,13 @@ notifications: | |||||||
|  |  | ||||||
| ### Logging | ### Logging | ||||||
|  |  | ||||||
| cmd-std-out controls whether commands output is echoed to StdOut. | `cmd-std-out` controls whether commands output is echoed to StdOut. | ||||||
|  |  | ||||||
| If logfile is not defined, the log file will be written to the config directory in the file `backy.log`. | If `logfile` is not defined, the log file will be written to the config directory in the file `backy.log`. | ||||||
|  |  | ||||||
| `console-disabled` controls whether the logging messages are echoed to StdOut. Default is false. | `console-disabled` controls whether the logging messages are echoed to StdOut. Default is false. | ||||||
|  |  | ||||||
| `verbose` basically does nothing as all necessary info is already output. | `verbose` prints out debugging messages. | ||||||
|  |  | ||||||
| ```yaml | ```yaml | ||||||
| logging: | logging: | ||||||
| @@ -144,7 +144,7 @@ logging: | |||||||
|  |  | ||||||
| [Vault](https://www.vaultproject.io/) can be used to get some configuration values and ENV variables securely. | [Vault](https://www.vaultproject.io/) can be used to get some configuration values and ENV variables securely. | ||||||
|  |  | ||||||
| ``` | ```yaml | ||||||
| vault: | vault: | ||||||
|   token: hvs.tXqcASvTP8wg92f7riyvGyuf |   token: hvs.tXqcASvTP8wg92f7riyvGyuf | ||||||
|   address: http://127.0.0.1:8200 |   address: http://127.0.0.1:8200 | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								docs/layouts/shortcodes/code.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								docs/layouts/shortcodes/code.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | {{ $file := .Get "file" | readFile }} | ||||||
|  | {{ $lang := .Get "language" }} | ||||||
|  | {{ (print "```" $lang "\n" $file "\n```") }} | ||||||
							
								
								
									
										24
									
								
								examples/example.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								examples/example.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | commands: | ||||||
|  |   stop-docker-container: | ||||||
|  |     cmd: docker | ||||||
|  |     Args: | ||||||
|  |       - compose | ||||||
|  |       - -f /some/path/to/docker-compose.yaml | ||||||
|  |       - down | ||||||
|  |     # if host is not defined, command will be run locally | ||||||
|  |     # The host has to be defined in either the config file or the SSH Config files | ||||||
|  |     host: some-host | ||||||
|  |     hooks: | ||||||
|  |       error: | ||||||
|  |         - some-other-command-when-failing | ||||||
|  |       success: | ||||||
|  |         - success-command | ||||||
|  |       final: | ||||||
|  |         - final-command | ||||||
|  |   backup-docker-container-script: | ||||||
|  |     cmd: /path/to/local/script | ||||||
|  |     # script file is input as stdin to SSH | ||||||
|  |     type: scriptFile # also can be script | ||||||
|  |     environment: | ||||||
|  |       - FOO=BAR | ||||||
|  |       - APP=$VAR | ||||||
							
								
								
									
										0
									
								
								getCommandHelp
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										0
									
								
								getCommandHelp
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
								
								
									
										92
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										92
									
								
								go.mod
									
									
									
									
									
								
							| @@ -1,76 +1,78 @@ | |||||||
| module git.andrewnw.xyz/CyberShell/backy | module git.andrewnw.xyz/CyberShell/backy | ||||||
|  |  | ||||||
| go 1.23 | go 1.23.0 | ||||||
|  |  | ||||||
| toolchain go1.23.6 | toolchain go1.23.7 | ||||||
|  |  | ||||||
| replace git.andrewnw.xyz/CyberShell/backy => /home/andrew/Projects/backy |  | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	github.com/aws/aws-sdk-go-v2/service/s3 v1.76.0 | 	github.com/aws/aws-sdk-go-v2/service/s3 v1.83.0 | ||||||
| 	github.com/dmarkham/enumer v1.5.11 | 	github.com/dmarkham/enumer v1.5.11 | ||||||
| 	github.com/go-co-op/gocron v1.37.0 | 	github.com/go-co-op/gocron v1.37.0 | ||||||
| 	github.com/hashicorp/vault/api v1.15.0 | 	github.com/google/uuid v1.6.0 | ||||||
|  | 	github.com/hashicorp/vault/api v1.20.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 v1.1.0 | ||||||
| 	github.com/knadh/koanf/providers/rawbytes v0.1.0 | 	github.com/knadh/koanf/providers/rawbytes v1.0.0 | ||||||
| 	github.com/knadh/koanf/v2 v2.1.2 | 	github.com/knadh/koanf/v2 v2.2.2 | ||||||
| 	github.com/mattn/go-isatty v0.0.20 | 	github.com/mattn/go-isatty v0.0.20 | ||||||
| 	github.com/minio/minio-go/v7 v7.0.84 | 	github.com/minio/minio-go/v7 v7.0.94 | ||||||
| 	github.com/mitchellh/go-homedir v1.1.0 | 	github.com/mitchellh/go-homedir v1.1.0 | ||||||
| 	github.com/nikoksr/notify v1.3.0 | 	github.com/nikoksr/notify v1.3.0 | ||||||
| 	github.com/pkg/errors v0.9.1 | 	github.com/pkg/errors v0.9.1 | ||||||
| 	github.com/rs/zerolog v1.33.0 | 	github.com/pkg/sftp v1.13.9 | ||||||
|  | 	github.com/rs/zerolog v1.34.0 | ||||||
| 	github.com/sethvargo/go-password v0.3.1 | 	github.com/sethvargo/go-password v0.3.1 | ||||||
| 	github.com/spf13/cobra v1.8.1 | 	github.com/spf13/cobra v1.9.1 | ||||||
| 	golang.org/x/crypto v0.33.0 | 	golang.org/x/crypto v0.40.0 | ||||||
| 	gopkg.in/natefinch/lumberjack.v2 v2.2.1 | 	gopkg.in/natefinch/lumberjack.v2 v2.2.1 | ||||||
| 	gopkg.in/yaml.v3 v3.0.1 | 	gopkg.in/yaml.v3 v3.0.1 | ||||||
| 	maunium.net/go/mautrix v0.23.0 | 	maunium.net/go/mautrix v0.24.1 | ||||||
| 	mvdan.cc/sh/v3 v3.10.0 | 	mvdan.cc/sh/v3 v3.12.0 | ||||||
| ) | ) | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	filippo.io/edwards25519 v1.1.0 // indirect | 	filippo.io/edwards25519 v1.1.0 // indirect | ||||||
| 	github.com/aws/aws-sdk-go-v2 v1.36.1 // indirect | 	github.com/aws/aws-sdk-go-v2 v1.36.5 // indirect | ||||||
| 	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 // indirect | 	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect | ||||||
| 	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.32 // indirect | 	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 // indirect | ||||||
| 	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.32 // indirect | 	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 // indirect | ||||||
| 	github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.32 // indirect | 	github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 // indirect | ||||||
| 	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 // indirect | 	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect | ||||||
| 	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.6 // indirect | 	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 // indirect | ||||||
| 	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.13 // indirect | 	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect | ||||||
| 	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.13 // indirect | 	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 // indirect | ||||||
| 	github.com/aws/smithy-go v1.22.2 // indirect | 	github.com/aws/smithy-go v1.22.4 // indirect | ||||||
| 	github.com/cenkalti/backoff/v4 v4.3.0 // indirect | 	github.com/cenkalti/backoff/v4 v4.3.0 // indirect | ||||||
| 	github.com/davecgh/go-spew v1.1.1 // indirect | 	github.com/davecgh/go-spew v1.1.1 // indirect | ||||||
| 	github.com/dustin/go-humanize v1.0.1 // indirect | 	github.com/dustin/go-humanize v1.0.1 // indirect | ||||||
| 	github.com/go-ini/ini v1.67.0 // indirect | 	github.com/go-ini/ini v1.67.0 // indirect | ||||||
| 	github.com/go-jose/go-jose/v4 v4.0.1 // indirect | 	github.com/go-jose/go-jose/v4 v4.1.1 // indirect | ||||||
| 	github.com/go-viper/mapstructure/v2 v2.2.1 // indirect | 	github.com/go-viper/mapstructure/v2 v2.3.0 // indirect | ||||||
| 	github.com/goccy/go-json v0.10.5 // indirect | 	github.com/goccy/go-json v0.10.5 // indirect | ||||||
| 	github.com/google/uuid v1.6.0 // indirect |  | ||||||
| 	github.com/hashicorp/errwrap v1.1.0 // indirect | 	github.com/hashicorp/errwrap v1.1.0 // indirect | ||||||
| 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect | 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect | ||||||
| 	github.com/hashicorp/go-multierror v1.1.1 // indirect | 	github.com/hashicorp/go-multierror v1.1.1 // indirect | ||||||
| 	github.com/hashicorp/go-retryablehttp v0.7.7 // indirect | 	github.com/hashicorp/go-retryablehttp v0.7.8 // indirect | ||||||
| 	github.com/hashicorp/go-rootcerts v1.0.2 // indirect | 	github.com/hashicorp/go-rootcerts v1.0.2 // indirect | ||||||
| 	github.com/hashicorp/go-secure-stdlib/parseutil v0.1.9 // indirect | 	github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect | ||||||
| 	github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect | 	github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect | ||||||
| 	github.com/hashicorp/go-sockaddr v1.0.7 // indirect | 	github.com/hashicorp/go-sockaddr v1.0.7 // indirect | ||||||
| 	github.com/hashicorp/hcl v1.0.0 // indirect | 	github.com/hashicorp/hcl v1.0.1-vault-7 // indirect | ||||||
| 	github.com/inconshreveable/mousetrap v1.1.0 // indirect | 	github.com/inconshreveable/mousetrap v1.1.0 // indirect | ||||||
| 	github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible // indirect | 	github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible // indirect | ||||||
| 	github.com/klauspost/compress v1.17.11 // indirect | 	github.com/klauspost/compress v1.18.0 // indirect | ||||||
| 	github.com/klauspost/cpuid/v2 v2.2.9 // indirect | 	github.com/klauspost/cpuid/v2 v2.3.0 // indirect | ||||||
| 	github.com/knadh/koanf/maps v0.1.1 // indirect | 	github.com/knadh/koanf/maps v0.1.2 // indirect | ||||||
|  | 	github.com/kr/fs v0.1.0 // indirect | ||||||
| 	github.com/mattn/go-colorable v0.1.14 // indirect | 	github.com/mattn/go-colorable v0.1.14 // indirect | ||||||
|  | 	github.com/minio/crc64nvme v1.0.2 // indirect | ||||||
| 	github.com/minio/md5-simd v1.1.2 // indirect | 	github.com/minio/md5-simd v1.1.2 // indirect | ||||||
| 	github.com/mitchellh/copystructure v1.2.0 // indirect | 	github.com/mitchellh/copystructure v1.2.0 // indirect | ||||||
| 	github.com/mitchellh/mapstructure v1.5.0 // indirect | 	github.com/mitchellh/mapstructure v1.5.0 // indirect | ||||||
| 	github.com/mitchellh/reflectwalk v1.0.2 // indirect | 	github.com/mitchellh/reflectwalk v1.0.2 // indirect | ||||||
| 	github.com/pascaldekloe/name v1.0.0 // indirect | 	github.com/pascaldekloe/name v1.0.1 // indirect | ||||||
|  | 	github.com/philhofer/fwd v1.2.0 // indirect | ||||||
| 	github.com/pmezard/go-difflib v1.0.0 // indirect | 	github.com/pmezard/go-difflib v1.0.0 // indirect | ||||||
| 	github.com/robfig/cron/v3 v3.0.1 // indirect | 	github.com/robfig/cron/v3 v3.0.1 // indirect | ||||||
| 	github.com/rs/xid v1.6.0 // indirect | 	github.com/rs/xid v1.6.0 // indirect | ||||||
| @@ -82,14 +84,16 @@ require ( | |||||||
| 	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.8.4 // indirect | 	github.com/tinylib/msgp v1.3.0 // indirect | ||||||
|  | 	go.mau.fi/util v0.8.8 // indirect | ||||||
| 	go.uber.org/atomic v1.11.0 // indirect | 	go.uber.org/atomic v1.11.0 // indirect | ||||||
| 	golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3 // indirect | 	go.yaml.in/yaml/v3 v3.0.4 // indirect | ||||||
| 	golang.org/x/mod v0.23.0 // indirect | 	golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect | ||||||
| 	golang.org/x/net v0.35.0 // indirect | 	golang.org/x/mod v0.26.0 // indirect | ||||||
| 	golang.org/x/sync v0.11.0 // indirect | 	golang.org/x/net v0.42.0 // indirect | ||||||
| 	golang.org/x/sys v0.30.0 // indirect | 	golang.org/x/sync v0.16.0 // indirect | ||||||
| 	golang.org/x/text v0.22.0 // indirect | 	golang.org/x/sys v0.34.0 // indirect | ||||||
| 	golang.org/x/time v0.10.0 // indirect | 	golang.org/x/text v0.27.0 // indirect | ||||||
| 	golang.org/x/tools v0.30.0 // indirect | 	golang.org/x/time v0.12.0 // indirect | ||||||
|  | 	golang.org/x/tools v0.35.0 // indirect | ||||||
| ) | ) | ||||||
|   | |||||||
							
								
								
									
										245
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										245
									
								
								go.sum
									
									
									
									
									
								
							| @@ -1,31 +1,31 @@ | |||||||
| filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= | ||||||
| filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= | ||||||
| github.com/aws/aws-sdk-go-v2 v1.36.1 h1:iTDl5U6oAhkNPba0e1t1hrwAo02ZMqbrGq4k5JBWM5E= | github.com/aws/aws-sdk-go-v2 v1.36.5 h1:0OF9RiEMEdDdZEMqF9MRjevyxAQcf6gY+E7vwBILFj0= | ||||||
| github.com/aws/aws-sdk-go-v2 v1.36.1/go.mod h1:5PMILGVKiW32oDzjj6RU52yrNrDPUHcbZQYr1sM7qmM= | github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0= | ||||||
| github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 h1:zAxi9p3wsZMIaVCdoiQp2uZ9k1LsZvmAnoTBeZPXom0= | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA= | ||||||
| github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8/go.mod h1:3XkePX5dSaxveLAYY7nsbsZZrKxCyEuE5pM4ziFxyGg= | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY= | ||||||
| github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.32 h1:BjUcr3X3K0wZPGFg2bxOWW3VPN8rkE3/61zhP+IHviA= | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 h1:SsytQyTMHMDPspp+spo7XwXTP44aJZZAC7fBV2C5+5s= | ||||||
| github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.32/go.mod h1:80+OGC/bgzzFFTUmcuwD0lb4YutwQeKLFpmt6hoWapU= | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo= | ||||||
| github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.32 h1:m1GeXHVMJsRsUAqG6HjZWx9dj7F5TR+cF1bjyfYyBd4= | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 h1:i2vNHQiXUvKhs3quBR6aqlgJaiaexz/aNvdCktW/kAM= | ||||||
| github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.32/go.mod h1:IitoQxGfaKdVLNg0hD8/DXmAqNy0H4K2H2Sf91ti8sI= | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8= | ||||||
| github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.32 h1:OIHj/nAhVzIXGzbAE+4XmZ8FPvro3THr6NlqErJc3wY= | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs= | ||||||
| github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.32/go.mod h1:LiBEsDo34OJXqdDlRGsilhlIiXR7DL+6Cx2f4p1EgzI= | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36/go.mod h1:gDhdAV6wL3PmPqBhiPbnlS447GoWs8HTTOYef9/9Inw= | ||||||
| github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 h1:D4oz8/CzT9bAEYtVhSBmFj2dNOtaHOtMKc2vHBwYizA= | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc= | ||||||
| github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2/go.mod h1:Za3IHqTQ+yNcRHxu1OFucBh0ACZT4j4VQFF0BqpZcLY= | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ= | ||||||
| github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.6 h1:cCBJaT7EeEojpJ4s7wTDbhZlHVJOgNHN7iw6qVurGaw= | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 h1:nAP2GYbfh8dd2zGZqFRSMlq+/F6cMPBUuCsGAMkN074= | ||||||
| github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.6/go.mod h1:WYH1ABybY7JK9TITPnk6ZlP7gQB8psI4c9qDmMsnLSA= | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4/go.mod h1:LT10DsiGjLWh4GbjInf9LQejkYEhBgBCjLG5+lvk4EE= | ||||||
| github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.13 h1:SYVGSFQHlchIcy6e7x12bsrxClCXSP5et8cqVhL8cuw= | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 h1:t0E6FzREdtCsiLIoLCWsYliNsRBgyGD/MCK571qk4MI= | ||||||
| github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.13/go.mod h1:kizuDaLX37bG5WZaoxGPQR/LNFXpxp0vsUnqfkWXfNE= | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4= | ||||||
| github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.13 h1:OBsrtam3rk8NfBEq7OLOMm5HtQ9Yyw32X4UQMya/wjw= | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 h1:qcLWgdhq45sDM9na4cvXax9dyLitn8EYBRl8Ak4XtG4= | ||||||
| github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.13/go.mod h1:3U4gFA5pmoCOja7aq4nSaIAGbaOHv2Yl2ug018cmC+Q= | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17/go.mod h1:M+jkjBFZ2J6DJrjMv2+vkBbuht6kxJYtJiwoVgX4p4U= | ||||||
| github.com/aws/aws-sdk-go-v2/service/s3 v1.76.0 h1:ehvUZNVrGA1Usa6yYo8A8pUqrigRelWXSbcCqYpRLeI= | github.com/aws/aws-sdk-go-v2/service/s3 v1.83.0 h1:5Y75q0RPQoAbieyOuGLhjV9P3txvYgXv2lg0UwJOfmE= | ||||||
| github.com/aws/aws-sdk-go-v2/service/s3 v1.76.0/go.mod h1:KuLNrwYJFaC2AVZ+CVVc12k9NyqwgWsoNNHjwqF6QNk= | github.com/aws/aws-sdk-go-v2/service/s3 v1.83.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU= | ||||||
| github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= | github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw= | ||||||
| github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= | github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= | ||||||
| github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= | ||||||
| github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= | ||||||
| github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= | ||||||
| github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= | ||||||
| github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= | ||||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||||
| @@ -40,19 +40,20 @@ github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b | |||||||
| github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY= | github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY= | ||||||
| github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= | github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= | ||||||
| github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= | github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= | ||||||
| github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= | github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI= | ||||||
| github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= | github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= | ||||||
| github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= | github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= | ||||||
| github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= | github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= | ||||||
| github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= | github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= | ||||||
| github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= | github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= | ||||||
| github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= | github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk= | ||||||
| github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= | github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= | ||||||
| github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= | ||||||
| github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= | ||||||
| github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= | ||||||
| github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= |  | ||||||
| github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | ||||||
|  | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= | ||||||
|  | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= | ||||||
| github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||||
| github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | ||||||
| github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||||
| @@ -65,20 +66,20 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1 | |||||||
| github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= | ||||||
| github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= | ||||||
| github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= | ||||||
| github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= | github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= | ||||||
| github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= | github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= | ||||||
| github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= | github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= | ||||||
| github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= | github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= | ||||||
| github.com/hashicorp/go-secure-stdlib/parseutil v0.1.9 h1:FW0YttEnUNDJ2WL9XcrrfteS1xW8u+sh4ggM8pN5isQ= | github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM= | ||||||
| github.com/hashicorp/go-secure-stdlib/parseutil v0.1.9/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= | github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= | ||||||
| github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= | ||||||
| github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= | ||||||
| github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= | github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= | ||||||
| github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= | github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= | ||||||
| github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= | github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= | ||||||
| github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= | github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= | ||||||
| github.com/hashicorp/vault/api v1.15.0 h1:O24FYQCWwhwKnF7CuSqP30S51rTV7vz1iACXE/pj5DA= | github.com/hashicorp/vault/api v1.20.0 h1:KQMHElgudOsr+IbJgmbjHnCTxEpKs9LnozA1D3nozU4= | ||||||
| github.com/hashicorp/vault/api v1.15.0/go.mod h1:+5YTO09JGn0u+b6ySD/LLVf8WkJCPLAL2Vkmrn2+CM8= | github.com/hashicorp/vault/api v1.20.0/go.mod h1:GZ4pcjfzoOWpkJ3ijHNpEoAxKEsBJnVljyTe3jM2Sms= | ||||||
| github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= | ||||||
| github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= | ||||||
| github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= | ||||||
| @@ -87,19 +88,21 @@ github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible | |||||||
| github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= | github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= | ||||||
| github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= | ||||||
| github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= | ||||||
| github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= | ||||||
| github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= | ||||||
| github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= | github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= | ||||||
| github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= | github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= | ||||||
| github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= | github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= | ||||||
| github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= | github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= | ||||||
| github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= | github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= | ||||||
| github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= | github.com/knadh/koanf/parsers/yaml v1.1.0 h1:3ltfm9ljprAHt4jxgeYLlFPmUaunuCgu1yILuTXRdM4= | ||||||
| github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY= | github.com/knadh/koanf/parsers/yaml v1.1.0/go.mod h1:HHmcHXUrp9cOPcuC+2wrr44GTUB0EC+PyfN3HZD9tFg= | ||||||
| github.com/knadh/koanf/providers/rawbytes v0.1.0 h1:dpzgu2KO6uf6oCb4aP05KDmKmAmI51k5pe8RYKQ0qME= | github.com/knadh/koanf/providers/rawbytes v1.0.0 h1:MrKDh/HksJlKJmaZjgs4r8aVBb/zsJyc/8qaSnzcdNI= | ||||||
| github.com/knadh/koanf/providers/rawbytes v0.1.0/go.mod h1:mMTB1/IcJ/yE++A2iEZbY1MLygX7vttU+C+S/YmPu9c= | github.com/knadh/koanf/providers/rawbytes v1.0.0/go.mod h1:KxwYJf1uezTKy6PBtfE+m725NGp4GPVA7XoNTJ/PtLo= | ||||||
| github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ= | github.com/knadh/koanf/v2 v2.2.2 h1:ghbduIkpFui3L587wavneC9e3WIliCgiCgdxYO/wd7A= | ||||||
| github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo= | github.com/knadh/koanf/v2 v2.2.2/go.mod h1:abWQc0cBXLSF/PSOMCB/SK+T13NXDsPvOksbpi5e/9Q= | ||||||
|  | github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= | ||||||
|  | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= | ||||||
| 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= | ||||||
| @@ -116,10 +119,12 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ | |||||||
| github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= | ||||||
| github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= | ||||||
| github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= | ||||||
|  | github.com/minio/crc64nvme v1.0.2 h1:6uO1UxGAD+kwqWWp7mBFsi5gAse66C4NXO8cmcVculg= | ||||||
|  | github.com/minio/crc64nvme v1.0.2/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= | ||||||
| github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= | github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= | ||||||
| github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= | github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= | ||||||
| github.com/minio/minio-go/v7 v7.0.84 h1:D1HVmAF8JF8Bpi6IU4V9vIEj+8pc+xU88EWMs2yed0E= | github.com/minio/minio-go/v7 v7.0.94 h1:1ZoksIKPyaSt64AVOyaQvhDOgVC3MfZsWM6mZXRUGtM= | ||||||
| github.com/minio/minio-go/v7 v7.0.84/go.mod h1:57YXpvc5l3rjPdhqNrDsvVlY0qPI6UTk1bflAe+9doY= | github.com/minio/minio-go/v7 v7.0.94/go.mod h1:71t2CqDt3ThzESgZUlU1rBN54mksGGlkLcFgguDnnAc= | ||||||
| github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= | ||||||
| github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= | ||||||
| github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= | ||||||
| @@ -130,32 +135,34 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx | |||||||
| github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= | ||||||
| github.com/nikoksr/notify v1.3.0 h1:UxzfxzAYGQD9a5JYLBTVx0lFMxeHCke3rPCkfWdPgLs= | github.com/nikoksr/notify v1.3.0 h1:UxzfxzAYGQD9a5JYLBTVx0lFMxeHCke3rPCkfWdPgLs= | ||||||
| github.com/nikoksr/notify v1.3.0/go.mod h1:Xor2hMmkvrCfkCKvXGbcrESez4brac2zQjhd6U2BbeM= | github.com/nikoksr/notify v1.3.0/go.mod h1:Xor2hMmkvrCfkCKvXGbcrESez4brac2zQjhd6U2BbeM= | ||||||
| github.com/pascaldekloe/name v1.0.0 h1:n7LKFgHixETzxpRv2R77YgPUFo85QHGZKrdaYm7eY5U= | github.com/pascaldekloe/name v1.0.1 h1:9lnXOHeqeHHnWLbKfH6X98+4+ETVqFqxN09UXSjcMb0= | ||||||
| github.com/pascaldekloe/name v1.0.0/go.mod h1:Z//MfYJnH4jVpQ9wkclwu2I2MkHmXTlT9wR5UZScttM= | github.com/pascaldekloe/name v1.0.1/go.mod h1:Z//MfYJnH4jVpQ9wkclwu2I2MkHmXTlT9wR5UZScttM= | ||||||
|  | github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= | ||||||
|  | github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= | ||||||
| github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= | ||||||
| github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | ||||||
| github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||||
|  | github.com/pkg/sftp v1.13.9 h1:4NGkvGudBL7GteO3m6qnaQ4pC0Kvf0onSVc9gR3EWBw= | ||||||
|  | github.com/pkg/sftp v1.13.9/go.mod h1:OBN7bVXdstkFFN/gdnHPUb5TE8eb8G1Rp9wCItqjkkA= | ||||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||||
| github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= | github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= | ||||||
| github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= | github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= | ||||||
| github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= | ||||||
| github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= | github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= | ||||||
| github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= | ||||||
| github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= | ||||||
| github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= |  | ||||||
| github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= | github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= | ||||||
| github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= | github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= | ||||||
| github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= | github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= | ||||||
| github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= | github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= | ||||||
| github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= | ||||||
| github.com/ryanuber/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 h1:WqrLTjo7X6AcVYfC6R7GtSyuUQR9hGyAj/f1PYQZCJU= | ||||||
| github.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5OptoxrMBcrXzvs= | github.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5OptoxrMBcrXzvs= | ||||||
| github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= | ||||||
| github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= | ||||||
| github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= |  | ||||||
| github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= | ||||||
| github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= | ||||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||||
| @@ -179,34 +186,100 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= | |||||||
| github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= | github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= | ||||||
| github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= | github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= | ||||||
| github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= | github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= | ||||||
| go.mau.fi/util v0.8.4 h1:mVKlJcXWfVo8ZW3f4vqtjGpqtZqJvX4ETekxawt2vnQ= | github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= | ||||||
| go.mau.fi/util v0.8.4/go.mod h1:MOfGTs1CBuK6ERTcSL4lb5YU7/ujz09eOPVEDckuazY= | github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= | ||||||
|  | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= | ||||||
|  | go.mau.fi/util v0.8.8 h1:OnuEEc/sIJFhnq4kFggiImUpcmnmL/xpvQMRu5Fiy5c= | ||||||
|  | go.mau.fi/util v0.8.8/go.mod h1:Y/kS3loxTEhy8Vill513EtPXr+CRDdae+Xj2BXXMy/c= | ||||||
| go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= | ||||||
| go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= | ||||||
| go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= | ||||||
| golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= | go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= | ||||||
| golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= | go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= | ||||||
| golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3 h1:qNgPs5exUA+G0C96DrPwNrvLSj7GT/9D+3WMWUcUg34= | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||||
| golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | ||||||
| golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= | ||||||
| golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= | ||||||
| golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= | ||||||
| golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= | ||||||
| golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= | golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= | ||||||
| golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= | ||||||
|  | golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc h1:TS73t7x3KarrNd5qAipmspBDS1rkMcgVG/fS1aRb4Rc= | ||||||
|  | golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= | ||||||
|  | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= | ||||||
|  | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= | ||||||
|  | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= | ||||||
|  | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= | ||||||
|  | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= | ||||||
|  | golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= | ||||||
|  | golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= | ||||||
|  | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||||
|  | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | ||||||
|  | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= | ||||||
|  | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= | ||||||
|  | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= | ||||||
|  | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= | ||||||
|  | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= | ||||||
|  | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= | ||||||
|  | golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= | ||||||
|  | golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= | ||||||
|  | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
|  | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
|  | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
|  | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= | ||||||
|  | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | ||||||
|  | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | ||||||
|  | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | ||||||
|  | golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= | ||||||
|  | golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= | ||||||
|  | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||||
|  | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
|  | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
|  | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
|  | golang.org/x/sys v0.5.0/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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||||
| golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||||
| golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||||
| golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= | golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= | ||||||
| golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= | golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= | ||||||
| golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= | ||||||
| golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||||
| golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | ||||||
| golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= | ||||||
| golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= | ||||||
|  | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= | ||||||
|  | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= | ||||||
|  | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= | ||||||
|  | golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= | ||||||
|  | golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= | ||||||
|  | golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= | ||||||
|  | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||||
|  | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||||
|  | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= | ||||||
|  | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | ||||||
|  | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= | ||||||
|  | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= | ||||||
|  | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= | ||||||
|  | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= | ||||||
|  | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= | ||||||
|  | golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= | ||||||
|  | golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= | ||||||
|  | golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= | ||||||
|  | golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= | ||||||
|  | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||||
|  | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||||||
|  | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= | ||||||
|  | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= | ||||||
|  | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= | ||||||
|  | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= | ||||||
|  | golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= | ||||||
|  | golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= | ||||||
|  | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||||
| gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||||
| gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | ||||||
| @@ -217,7 +290,7 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs | |||||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||||
| maunium.net/go/mautrix v0.23.0 h1:HNlR19eew5lvrNSL2muhExaGhYdaGk5FfEiA82QqUP4= | maunium.net/go/mautrix v0.24.1 h1:09/xi4qTeA03g1n/DPmmqAlT8Cx4QrgwiPlmLVzA9AU= | ||||||
| maunium.net/go/mautrix v0.23.0/go.mod h1:AGnnaz3ylGikUo1I1MJVn9QLsl2No1/ZNnGDyO0QD5s= | maunium.net/go/mautrix v0.24.1/go.mod h1:Xy6o+pXmbqmgWsUWh15EQ1eozjC+k/VT/7kloByv9PI= | ||||||
| mvdan.cc/sh/v3 v3.10.0 h1:v9z7N1DLZ7owyLM/SXZQkBSXcwr2IGMm2LY2pmhVXj4= | mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI= | ||||||
| mvdan.cc/sh/v3 v3.10.0/go.mod h1:z/mSSVyLFGZzqb3ZIKojjyqIx/xbmz/UHdCSv9HmqXY= | mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg= | ||||||
|   | |||||||
							
								
								
									
										149
									
								
								pkg/backy/allowedexternaldirectives_enumer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								pkg/backy/allowedexternaldirectives_enumer.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,149 @@ | |||||||
|  | // Code generated by "enumer -linecomment -yaml -text -json -type=AllowedExternalDirectives"; DO NOT EDIT. | ||||||
|  |  | ||||||
|  | package backy | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const _AllowedExternalDirectivesName = "DefaultExternalDirvaultvault-envvault-filevault-file-envfile-envfileenv" | ||||||
|  |  | ||||||
|  | var _AllowedExternalDirectivesIndex = [...]uint8{0, 18, 23, 32, 42, 56, 64, 68, 71} | ||||||
|  |  | ||||||
|  | const _AllowedExternalDirectivesLowerName = "defaultexternaldirvaultvault-envvault-filevault-file-envfile-envfileenv" | ||||||
|  |  | ||||||
|  | func (i AllowedExternalDirectives) String() string { | ||||||
|  | 	if i < 0 || i >= AllowedExternalDirectives(len(_AllowedExternalDirectivesIndex)-1) { | ||||||
|  | 		return fmt.Sprintf("AllowedExternalDirectives(%d)", i) | ||||||
|  | 	} | ||||||
|  | 	return _AllowedExternalDirectivesName[_AllowedExternalDirectivesIndex[i]:_AllowedExternalDirectivesIndex[i+1]] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // An "invalid array index" compiler error signifies that the constant values have changed. | ||||||
|  | // Re-run the stringer command to generate them again. | ||||||
|  | func _AllowedExternalDirectivesNoOp() { | ||||||
|  | 	var x [1]struct{} | ||||||
|  | 	_ = x[DefaultExternalDir-(0)] | ||||||
|  | 	_ = x[AllowedExternalDirectiveVault-(1)] | ||||||
|  | 	_ = x[AllowedExternalDirectiveVaultEnv-(2)] | ||||||
|  | 	_ = x[AllowedExternalDirectiveVaultFile-(3)] | ||||||
|  | 	_ = x[AllowedExternalDirectiveAll-(4)] | ||||||
|  | 	_ = x[AllowedExternalDirectiveFileEnv-(5)] | ||||||
|  | 	_ = x[AllowedExternalDirectiveFile-(6)] | ||||||
|  | 	_ = x[AllowedExternalDirectiveEnv-(7)] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var _AllowedExternalDirectivesValues = []AllowedExternalDirectives{DefaultExternalDir, AllowedExternalDirectiveVault, AllowedExternalDirectiveVaultEnv, AllowedExternalDirectiveVaultFile, AllowedExternalDirectiveAll, AllowedExternalDirectiveFileEnv, AllowedExternalDirectiveFile, AllowedExternalDirectiveEnv} | ||||||
|  |  | ||||||
|  | var _AllowedExternalDirectivesNameToValueMap = map[string]AllowedExternalDirectives{ | ||||||
|  | 	_AllowedExternalDirectivesName[0:18]:       DefaultExternalDir, | ||||||
|  | 	_AllowedExternalDirectivesLowerName[0:18]:  DefaultExternalDir, | ||||||
|  | 	_AllowedExternalDirectivesName[18:23]:      AllowedExternalDirectiveVault, | ||||||
|  | 	_AllowedExternalDirectivesLowerName[18:23]: AllowedExternalDirectiveVault, | ||||||
|  | 	_AllowedExternalDirectivesName[23:32]:      AllowedExternalDirectiveVaultEnv, | ||||||
|  | 	_AllowedExternalDirectivesLowerName[23:32]: AllowedExternalDirectiveVaultEnv, | ||||||
|  | 	_AllowedExternalDirectivesName[32:42]:      AllowedExternalDirectiveVaultFile, | ||||||
|  | 	_AllowedExternalDirectivesLowerName[32:42]: AllowedExternalDirectiveVaultFile, | ||||||
|  | 	_AllowedExternalDirectivesName[42:56]:      AllowedExternalDirectiveAll, | ||||||
|  | 	_AllowedExternalDirectivesLowerName[42:56]: AllowedExternalDirectiveAll, | ||||||
|  | 	_AllowedExternalDirectivesName[56:64]:      AllowedExternalDirectiveFileEnv, | ||||||
|  | 	_AllowedExternalDirectivesLowerName[56:64]: AllowedExternalDirectiveFileEnv, | ||||||
|  | 	_AllowedExternalDirectivesName[64:68]:      AllowedExternalDirectiveFile, | ||||||
|  | 	_AllowedExternalDirectivesLowerName[64:68]: AllowedExternalDirectiveFile, | ||||||
|  | 	_AllowedExternalDirectivesName[68:71]:      AllowedExternalDirectiveEnv, | ||||||
|  | 	_AllowedExternalDirectivesLowerName[68:71]: AllowedExternalDirectiveEnv, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var _AllowedExternalDirectivesNames = []string{ | ||||||
|  | 	_AllowedExternalDirectivesName[0:18], | ||||||
|  | 	_AllowedExternalDirectivesName[18:23], | ||||||
|  | 	_AllowedExternalDirectivesName[23:32], | ||||||
|  | 	_AllowedExternalDirectivesName[32:42], | ||||||
|  | 	_AllowedExternalDirectivesName[42:56], | ||||||
|  | 	_AllowedExternalDirectivesName[56:64], | ||||||
|  | 	_AllowedExternalDirectivesName[64:68], | ||||||
|  | 	_AllowedExternalDirectivesName[68:71], | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // AllowedExternalDirectivesString retrieves an enum value from the enum constants string name. | ||||||
|  | // Throws an error if the param is not part of the enum. | ||||||
|  | func AllowedExternalDirectivesString(s string) (AllowedExternalDirectives, error) { | ||||||
|  | 	if val, ok := _AllowedExternalDirectivesNameToValueMap[s]; ok { | ||||||
|  | 		return val, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if val, ok := _AllowedExternalDirectivesNameToValueMap[strings.ToLower(s)]; ok { | ||||||
|  | 		return val, nil | ||||||
|  | 	} | ||||||
|  | 	return 0, fmt.Errorf("%s does not belong to AllowedExternalDirectives values", s) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // AllowedExternalDirectivesValues returns all values of the enum | ||||||
|  | func AllowedExternalDirectivesValues() []AllowedExternalDirectives { | ||||||
|  | 	return _AllowedExternalDirectivesValues | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // AllowedExternalDirectivesStrings returns a slice of all String values of the enum | ||||||
|  | func AllowedExternalDirectivesStrings() []string { | ||||||
|  | 	strs := make([]string, len(_AllowedExternalDirectivesNames)) | ||||||
|  | 	copy(strs, _AllowedExternalDirectivesNames) | ||||||
|  | 	return strs | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IsAAllowedExternalDirectives returns "true" if the value is listed in the enum definition. "false" otherwise | ||||||
|  | func (i AllowedExternalDirectives) IsAAllowedExternalDirectives() bool { | ||||||
|  | 	for _, v := range _AllowedExternalDirectivesValues { | ||||||
|  | 		if i == v { | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // MarshalJSON implements the json.Marshaler interface for AllowedExternalDirectives | ||||||
|  | func (i AllowedExternalDirectives) MarshalJSON() ([]byte, error) { | ||||||
|  | 	return json.Marshal(i.String()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // UnmarshalJSON implements the json.Unmarshaler interface for AllowedExternalDirectives | ||||||
|  | func (i *AllowedExternalDirectives) UnmarshalJSON(data []byte) error { | ||||||
|  | 	var s string | ||||||
|  | 	if err := json.Unmarshal(data, &s); err != nil { | ||||||
|  | 		return fmt.Errorf("AllowedExternalDirectives should be a string, got %s", data) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var err error | ||||||
|  | 	*i, err = AllowedExternalDirectivesString(s) | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // MarshalText implements the encoding.TextMarshaler interface for AllowedExternalDirectives | ||||||
|  | func (i AllowedExternalDirectives) MarshalText() ([]byte, error) { | ||||||
|  | 	return []byte(i.String()), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // UnmarshalText implements the encoding.TextUnmarshaler interface for AllowedExternalDirectives | ||||||
|  | func (i *AllowedExternalDirectives) UnmarshalText(text []byte) error { | ||||||
|  | 	var err error | ||||||
|  | 	*i, err = AllowedExternalDirectivesString(string(text)) | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // MarshalYAML implements a YAML Marshaler for AllowedExternalDirectives | ||||||
|  | func (i AllowedExternalDirectives) MarshalYAML() (interface{}, error) { | ||||||
|  | 	return i.String(), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // UnmarshalYAML implements a YAML Unmarshaler for AllowedExternalDirectives | ||||||
|  | func (i *AllowedExternalDirectives) UnmarshalYAML(unmarshal func(interface{}) error) error { | ||||||
|  | 	var s string | ||||||
|  | 	if err := unmarshal(&s); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var err error | ||||||
|  | 	*i, err = AllowedExternalDirectivesString(s) | ||||||
|  | 	return err | ||||||
|  | } | ||||||
| @@ -11,6 +11,8 @@ import ( | |||||||
| 	"io" | 	"io" | ||||||
| 	"os" | 	"os" | ||||||
| 	"os/exec" | 	"os/exec" | ||||||
|  | 	"strings" | ||||||
|  | 	"sync" | ||||||
| 	"text/template" | 	"text/template" | ||||||
|  |  | ||||||
| 	"embed" | 	"embed" | ||||||
| @@ -25,6 +27,124 @@ var requiredKeys = []string{"commands"} | |||||||
|  |  | ||||||
| var Sprintf = fmt.Sprintf | var Sprintf = fmt.Sprintf | ||||||
|  |  | ||||||
|  | type CommandExecutor interface { | ||||||
|  | 	Run(cmd *Command, opts *ConfigOpts, logger zerolog.Logger) ([]string, error) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type OutputHandler interface { | ||||||
|  | 	CollectOutput(buf *bytes.Buffer, commandName string, logger zerolog.Logger, wantOutput bool) []string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type EnvInjector interface { | ||||||
|  | 	Inject(cmd *Command, opts *ConfigOpts) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type PackageCommandExecutor struct{} | ||||||
|  |  | ||||||
|  | func (e *PackageCommandExecutor) Run(cmd *Command, opts *ConfigOpts, logger zerolog.Logger) ([]string, error) { | ||||||
|  | 	var ( | ||||||
|  | 		ArgsStr       string | ||||||
|  | 		cmdOutBuf     bytes.Buffer | ||||||
|  | 		outputArr     []string | ||||||
|  | 		cmdOutWriters io.Writer | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	for _, v := range cmd.Args { | ||||||
|  | 		ArgsStr += fmt.Sprintf(" %s", v) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Example: Check version operation | ||||||
|  | 	if cmd.PackageOperation == PackageOperationCheckVersion { | ||||||
|  | 		logger.Info().Msg("Checking package versions") | ||||||
|  |  | ||||||
|  | 		logger.Info().Msg("") | ||||||
|  | 		for _, p := range cmd.Packages { | ||||||
|  | 			logger.Info().Str("package", p.Name).Msg("Checking installed and remote package versions") | ||||||
|  | 		} | ||||||
|  | 		opts.Logger.Info().Msg("") | ||||||
|  |  | ||||||
|  | 		// Execute the package version command | ||||||
|  | 		execCmd := exec.Command(cmd.Cmd, cmd.Args...) | ||||||
|  | 		cmdOutWriters = io.MultiWriter(&cmdOutBuf) | ||||||
|  |  | ||||||
|  | 		if IsCmdStdOutEnabled() { | ||||||
|  | 			cmdOutWriters = io.MultiWriter(os.Stdout, &cmdOutBuf) | ||||||
|  | 		} | ||||||
|  | 		execCmd.Stdout = cmdOutWriters | ||||||
|  | 		execCmd.Stderr = cmdOutWriters | ||||||
|  |  | ||||||
|  | 		if err := execCmd.Run(); err != nil { | ||||||
|  | 			return nil, fmt.Errorf("error running command %s: %w", ArgsStr, err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return parsePackageVersion(cmdOutBuf.String(), logger, cmd, cmdOutBuf) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Other package operations (install, upgrade, etc.) can be handled here | ||||||
|  |  | ||||||
|  | 	// Default: run as a shell command | ||||||
|  | 	execCmd := exec.Command(cmd.Cmd, cmd.Args...) | ||||||
|  | 	execCmd.Stdout = &cmdOutBuf | ||||||
|  | 	execCmd.Stderr = &cmdOutBuf | ||||||
|  | 	err := execCmd.Run() | ||||||
|  | 	outputArr = logCommandOutput(cmd, cmdOutBuf, logger, outputArr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.Error().Err(fmt.Errorf("error running package command %s: %w", cmd.Name, err)).Send() | ||||||
|  | 		return outputArr, err | ||||||
|  | 	} | ||||||
|  | 	return outputArr, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type LocalCommandExecutor struct{} | ||||||
|  |  | ||||||
|  | func (e *LocalCommandExecutor) Run(cmd *Command, opts *ConfigOpts, logger zerolog.Logger) ([]string, error) { | ||||||
|  | 	var ( | ||||||
|  | 		ArgsStr   string | ||||||
|  | 		cmdOutBuf bytes.Buffer | ||||||
|  | 		outputArr []string | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	for _, v := range cmd.Args { | ||||||
|  | 		ArgsStr += fmt.Sprintf(" %s", v) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Build the command | ||||||
|  | 	var localCMD *exec.Cmd | ||||||
|  | 	if cmd.Shell != "" { | ||||||
|  | 		logger.Info().Str("Command", fmt.Sprintf("Running command %s on local machine in %s", cmd.Name, cmd.Shell)).Send() | ||||||
|  | 		ArgsStr = fmt.Sprintf("%s %s", cmd.Cmd, ArgsStr) | ||||||
|  | 		localCMD = exec.Command(cmd.Shell, "-c", ArgsStr) | ||||||
|  | 	} else { | ||||||
|  | 		localCMD = exec.Command(cmd.Cmd, cmd.Args...) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Set working directory | ||||||
|  | 	if cmd.Dir != nil { | ||||||
|  | 		localCMD.Dir = *cmd.Dir | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Inject environment variables (extract this to an EnvInjector if desired) | ||||||
|  | 	// injectEnvIntoLocalCMD(...) | ||||||
|  |  | ||||||
|  | 	// Set output writers | ||||||
|  | 	cmdOutWriters := io.MultiWriter(&cmdOutBuf) | ||||||
|  | 	if IsCmdStdOutEnabled() { | ||||||
|  | 		cmdOutWriters = io.MultiWriter(os.Stdout, &cmdOutBuf) | ||||||
|  | 	} | ||||||
|  | 	localCMD.Stdout = cmdOutWriters | ||||||
|  | 	localCMD.Stderr = cmdOutWriters | ||||||
|  |  | ||||||
|  | 	// Run the command | ||||||
|  | 	err := localCMD.Run() | ||||||
|  | 	outputArr = logCommandOutput(cmd, cmdOutBuf, logger, outputArr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.Error().Err(fmt.Errorf("error when running cmd %s: %w", cmd.Name, err)).Send() | ||||||
|  | 		return outputArr, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return outputArr, nil | ||||||
|  | } | ||||||
|  |  | ||||||
| // RunCmd runs a Command. | // RunCmd runs a Command. | ||||||
| // The environment of local commands will be the machine's environment plus any extra | // The environment of local commands will be the machine's environment plus any extra | ||||||
| // variables specified in the Env file or Environment. | // variables specified in the Env file or Environment. | ||||||
| @@ -34,7 +154,7 @@ var Sprintf = fmt.Sprintf | |||||||
| func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([]string, error) { | func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([]string, error) { | ||||||
|  |  | ||||||
| 	var ( | 	var ( | ||||||
| 		ArgsStr       string // concatenating the arguments | 		ArgsStr       string | ||||||
| 		cmdOutBuf     bytes.Buffer | 		cmdOutBuf     bytes.Buffer | ||||||
| 		cmdOutWriters io.Writer | 		cmdOutWriters io.Writer | ||||||
| 		errSSH        error | 		errSSH        error | ||||||
| @@ -54,38 +174,29 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ | |||||||
| 		ArgsStr += fmt.Sprintf(" %s", v) | 		ArgsStr += fmt.Sprintf(" %s", v) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if command.Type == UserCT { | 	if command.Type == UserCommandType { | ||||||
| 		if command.UserOperation == "password" { | 		if command.UserOperation == "password" { | ||||||
| 			cmdCtxLogger.Info().Str("password", command.UserPassword).Msg("user password to be updated") | 			cmdCtxLogger.Info().Str("password", command.UserPassword).Msg("user password to be updated") | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if command.Host != nil { | 	if !IsHostLocal(command.Host) { | ||||||
| 		outputArr, errSSH = command.RunCmdSSH(cmdCtxLogger, opts) |  | ||||||
|  | 		outputArr, errSSH = command.RunCmdOnHost(cmdCtxLogger, opts) | ||||||
| 		if errSSH != nil { | 		if errSSH != nil { | ||||||
| 			return outputArr, errSSH | 			return outputArr, errSSH | ||||||
| 		} | 		} | ||||||
| 	} else { | 	} else { | ||||||
|  |  | ||||||
| 		// Handle package operations | 		switch command.Type { | ||||||
| 		if command.Type == PackageCT && command.PackageOperation == PackOpCheckVersion { | 		case PackageCommandType: | ||||||
| 			cmdCtxLogger.Info().Str("package", command.PackageName).Msg("Checking package versions") | 			var executor PackageCommandExecutor | ||||||
|  | 			return executor.Run(command, opts, cmdCtxLogger) | ||||||
| 			// 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 localCMD *exec.Cmd | ||||||
| 		if command.Type == RemoteScriptCT { |  | ||||||
|  | 		if command.Type == RemoteScriptCommandType { | ||||||
| 			script, err := command.Fetcher.Fetch(command.Cmd) | 			script, err := command.Fetcher.Fetch(command.Cmd) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return nil, err | 				return nil, err | ||||||
| @@ -95,15 +206,15 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ | |||||||
| 				command.Shell = "sh" | 				command.Shell = "sh" | ||||||
| 			} | 			} | ||||||
| 			localCMD = exec.Command(command.Shell, command.Args...) | 			localCMD = exec.Command(command.Shell, command.Args...) | ||||||
| 			injectEnvIntoLocalCMD(envVars, localCMD, cmdCtxLogger) | 			injectEnvIntoLocalCMD(envVars, localCMD, cmdCtxLogger, opts) | ||||||
|  |  | ||||||
| 			cmdOutWriters = io.MultiWriter(&cmdOutBuf) | 			cmdOutWriters = io.MultiWriter(&cmdOutBuf) | ||||||
|  |  | ||||||
| 			if IsCmdStdOutEnabled() { | 			if IsCmdStdOutEnabled() { | ||||||
| 				cmdOutWriters = io.MultiWriter(os.Stdout, &cmdOutBuf) | 				cmdOutWriters = io.MultiWriter(os.Stdout, &cmdOutBuf) | ||||||
| 			} | 			} | ||||||
| 			if command.OutputFile != "" { | 			if command.Output.File != "" { | ||||||
| 				file, err := os.Create(command.OutputFile) | 				file, err := os.Create(command.Output.File) | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
| 					return nil, fmt.Errorf("error creating output file: %w", err) | 					return nil, fmt.Errorf("error creating output file: %w", err) | ||||||
| 				} | 				} | ||||||
| @@ -136,7 +247,7 @@ 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.OutputToLog { | 				if command.Output.ToLog { | ||||||
| 					cmdCtxLogger.Info().Fields(outMap).Send() | 					cmdCtxLogger.Info().Fields(outMap).Send() | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| @@ -144,6 +255,7 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		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() | ||||||
|  |  | ||||||
| @@ -156,8 +268,10 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ | |||||||
| 			cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s on local machine", command.Name)).Send() | 			cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s on local machine", command.Name)).Send() | ||||||
|  |  | ||||||
| 			// execute package commands in a shell | 			// execute package commands in a shell | ||||||
| 			if command.Type == PackageCT { | 			if command.Type == PackageCommandType { | ||||||
| 				cmdCtxLogger.Info().Str("package", command.PackageName).Msg("Executing package command") | 				for _, p := range command.Packages { | ||||||
|  | 					cmdCtxLogger.Info().Str("packages", p.Name).Msg("Executing package command") | ||||||
|  | 				} | ||||||
| 				ArgsStr = fmt.Sprintf("%s %s", command.Cmd, ArgsStr) | 				ArgsStr = fmt.Sprintf("%s %s", command.Cmd, ArgsStr) | ||||||
| 				localCMD = exec.Command("/bin/sh", "-c", ArgsStr) | 				localCMD = exec.Command("/bin/sh", "-c", ArgsStr) | ||||||
| 			} else { | 			} else { | ||||||
| @@ -165,11 +279,17 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		if command.Type == UserCommandType { | ||||||
|  | 			if command.UserOperation == "password" { | ||||||
|  | 				localCMD.Stdin = command.stdin | ||||||
|  | 				cmdCtxLogger.Info().Str("password", command.UserPassword).Msg("user password to be updated") | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
| 		if command.Dir != nil { | 		if command.Dir != nil { | ||||||
| 			localCMD.Dir = *command.Dir | 			localCMD.Dir = *command.Dir | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		injectEnvIntoLocalCMD(envVars, localCMD, cmdCtxLogger) | 		injectEnvIntoLocalCMD(envVars, localCMD, cmdCtxLogger, opts) | ||||||
|  |  | ||||||
| 		cmdOutWriters = io.MultiWriter(&cmdOutBuf) | 		cmdOutWriters = io.MultiWriter(&cmdOutBuf) | ||||||
|  |  | ||||||
| @@ -182,38 +302,84 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ | |||||||
|  |  | ||||||
| 		err = localCMD.Run() | 		err = localCMD.Run() | ||||||
|  |  | ||||||
| 		outScanner := bufio.NewScanner(&cmdOutBuf) | 		outputArr = logCommandOutput(command, cmdOutBuf, cmdCtxLogger, outputArr) | ||||||
|  |  | ||||||
| 		for outScanner.Scan() { |  | ||||||
| 			outMap := make(map[string]interface{}) |  | ||||||
| 			outMap["cmd"] = command.Cmd |  | ||||||
| 			outMap["output"] = outScanner.Text() |  | ||||||
|  |  | ||||||
| 			if str, ok := outMap["output"].(string); ok { |  | ||||||
| 				outputArr = append(outputArr, str) |  | ||||||
| 			} |  | ||||||
| 			// if command.GetOutput { |  | ||||||
| 			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() | ||||||
| 			return outputArr, err | 			return outputArr, err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		if command.Type == UserCommandType { | ||||||
|  |  | ||||||
|  | 			if command.UserOperation == "add" { | ||||||
|  | 				if command.UserSshPubKeys != nil { | ||||||
|  | 					var ( | ||||||
|  | 						authorizedKeysFile *os.File | ||||||
|  | 						err                error | ||||||
|  | 						userHome           []byte | ||||||
|  | 					) | ||||||
|  |  | ||||||
|  | 					cmdCtxLogger.Info().Msg("adding SSH Keys") | ||||||
|  |  | ||||||
|  | 					localCMD := exec.Command(fmt.Sprintf("grep \"%s\" /etc/passwd | cut -d: -f6", command.Username)) | ||||||
|  | 					userHome, err = localCMD.CombinedOutput() | ||||||
|  | 					if err != nil { | ||||||
|  | 						return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error finding user home from /etc/passwd: %v", err) | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					command.UserHome = strings.TrimSpace(string(userHome)) | ||||||
|  | 					userSshDir := fmt.Sprintf("%s/.ssh", command.UserHome) | ||||||
|  |  | ||||||
|  | 					if _, err := os.Stat(userSshDir); os.IsNotExist(err) { | ||||||
|  | 						err := os.MkdirAll(userSshDir, 0700) | ||||||
|  | 						if err != nil { | ||||||
|  | 							return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error creating directory %s %v", userSshDir, err) | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					if _, err := os.Stat(fmt.Sprintf("%s/authorized_keys", userSshDir)); os.IsNotExist(err) { | ||||||
|  | 						_, err := os.Create(fmt.Sprintf("%s/authorized_keys", userSshDir)) | ||||||
|  | 						if err != nil { | ||||||
|  | 							return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error creating file %s/authorized_keys: %v", userSshDir, err) | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					authorizedKeysFile, err = os.OpenFile(fmt.Sprintf("%s/authorized_keys", userSshDir), 0700, os.ModeAppend) | ||||||
|  | 					if err != nil { | ||||||
|  | 						return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error opening file %s/authorized_keys: %v", userSshDir, err) | ||||||
|  | 					} | ||||||
|  | 					defer authorizedKeysFile.Close() | ||||||
|  | 					for _, k := range command.UserSshPubKeys { | ||||||
|  | 						buf := bytes.NewBufferString(k) | ||||||
|  | 						cmdCtxLogger.Info().Str("key", k).Msg("adding SSH key") | ||||||
|  | 						if _, err := authorizedKeysFile.ReadFrom(buf); err != nil { | ||||||
|  | 							return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error adding to authorized keys: %v", err) | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 					localCMD = exec.Command(fmt.Sprintf("chown -R %s:%s %s", command.Username, command.Username, userHome)) | ||||||
|  | 					_, err = localCMD.CombinedOutput() | ||||||
|  | 					if err != nil { | ||||||
|  | 						return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), err | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 	return outputArr, nil | 	return outputArr, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<- CmdResult, opts *ConfigOpts) { | func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<- string, opts *ConfigOpts) { | ||||||
| 	for list := range jobs { | 	for list := range jobs { | ||||||
| 		fieldsMap := map[string]interface{}{"list": list.Name} | 		fieldsMap := map[string]interface{}{"list": list.Name} | ||||||
| 		var cmdLogger zerolog.Logger | 		var cmdLogger zerolog.Logger | ||||||
|  | 		var commandExecuted *Command | ||||||
| 		var cmdsRan []string | 		var cmdsRan []string | ||||||
| 		var outStructArr []outStruct | 		var outStructArr []outStruct | ||||||
| 		var hasError bool // Tracks if any command in the list failed | 		var hasError bool // Tracks if any command in the list failed | ||||||
|  |  | ||||||
| 		for _, cmd := range list.Order { | 		for _, cmd := range list.Order { | ||||||
| 			cmdToRun := opts.Cmds[cmd] | 			cmdToRun := opts.Cmds[cmd] | ||||||
|  | 			commandExecuted = cmdToRun | ||||||
| 			currentCmd := cmdToRun.Name | 			currentCmd := cmdToRun.Name | ||||||
| 			fieldsMap["cmd"] = currentCmd | 			fieldsMap["cmd"] = currentCmd | ||||||
| 			cmdLogger = cmdToRun.GenerateLogger(opts) | 			cmdLogger = cmdToRun.GenerateLogger(opts) | ||||||
| @@ -224,23 +390,21 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<- | |||||||
|  |  | ||||||
| 			if runErr != nil { | 			if runErr != nil { | ||||||
|  |  | ||||||
| 				// Log the error and send a failed result |  | ||||||
| 				cmdLogger.Err(runErr).Send() | 				cmdLogger.Err(runErr).Send() | ||||||
| 				results <- CmdResult{CmdName: cmd, ListName: list.Name, Error: runErr} |  | ||||||
|  |  | ||||||
| 				// Execute error hooks for the failed command |  | ||||||
| 				cmdToRun.ExecuteHooks("error", opts) | 				cmdToRun.ExecuteHooks("error", opts) | ||||||
|  |  | ||||||
| 				// Notify failure | 				// Notify failure | ||||||
| 				if list.NotifyConfig != nil { | 				if list.NotifyConfig != nil { | ||||||
| 					notifyError(cmdLogger, msgTemps, list, cmdsRan, outStructArr, runErr, cmdToRun) | 					notifyError(cmdLogger, msgTemps, list, cmdsRan, outStructArr, runErr, cmdToRun) | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
|  | 				// Execute error hooks for the failed command | ||||||
| 				hasError = true | 				hasError = true | ||||||
| 				break | 				break | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			// Collect output if required | 			if list.GetCommandOutputInNotificationsOnSuccess || cmdToRun.Output.InList { | ||||||
| 			if list.GetOutput || cmdToRun.GetOutput { |  | ||||||
| 				outStructArr = append(outStructArr, outStruct{ | 				outStructArr = append(outStructArr, outStruct{ | ||||||
| 					CmdName:     currentCmd, | 					CmdName:     currentCmd, | ||||||
| 					CmdExecuted: currentCmd, | 					CmdExecuted: currentCmd, | ||||||
| @@ -249,44 +413,242 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<- | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Notify success if no errors occurred | 		if !hasError && list.NotifyConfig != nil && list.Notify.OnFailure { | ||||||
| 		if !hasError && list.NotifyConfig != nil && (list.NotifyOnSuccess || list.GetOutput) { |  | ||||||
| 			notifySuccess(cmdLogger, msgTemps, list, cmdsRan, outStructArr) | 			notifySuccess(cmdLogger, msgTemps, list, cmdsRan, outStructArr) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Execute success and final hooks for all commands | 		if !hasError { | ||||||
|  | 			commandExecuted.ExecuteHooks("success", opts) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		commandExecuted.ExecuteHooks("final", opts) | ||||||
|  |  | ||||||
|  | 		results <- "done" | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | func cmdListWorkerWithHosts(msgTemps *msgTemplates, jobs <-chan *CmdList, hosts <-chan *Host, results chan<- string, opts *ConfigOpts) { | ||||||
|  | 	for list := range jobs { | ||||||
|  | 		fieldsMap := map[string]interface{}{"list": list.Name} | ||||||
|  | 		var cmdLogger zerolog.Logger | ||||||
|  | 		var commandExecuted *Command | ||||||
|  | 		var cmdsRan []string | ||||||
|  | 		var outStructArr []outStruct | ||||||
|  | 		var hasError bool // Tracks if any command in the list failed | ||||||
|  |  | ||||||
|  | 		for host := range hosts { | ||||||
|  |  | ||||||
| 			for _, cmd := range list.Order { | 			for _, cmd := range list.Order { | ||||||
| 				cmdToRun := opts.Cmds[cmd] | 				cmdToRun := opts.Cmds[cmd] | ||||||
|  | 				if cmdToRun.Host != host.Host { | ||||||
|  | 					cmdToRun.Host = host.Host | ||||||
|  | 					cmdToRun.RemoteHost = host | ||||||
|  | 				} | ||||||
|  | 				commandExecuted = cmdToRun | ||||||
|  | 				currentCmd := cmdToRun.Name | ||||||
|  | 				fieldsMap["cmd"] = currentCmd | ||||||
|  | 				cmdLogger = cmdToRun.GenerateLogger(opts) | ||||||
|  | 				cmdLogger.Info().Fields(fieldsMap).Send() | ||||||
|  |  | ||||||
| 			// Execute success hooks if the command succeeded | 				outputArr, runErr := cmdToRun.RunCmd(cmdLogger, opts) | ||||||
| 			if !hasError || cmdsRanContains(cmd, cmdsRan) { | 				cmdsRan = append(cmdsRan, cmd) | ||||||
| 				cmdToRun.ExecuteHooks("success", opts) |  | ||||||
|  | 				if runErr != nil { | ||||||
|  |  | ||||||
|  | 					cmdLogger.Err(runErr).Send() | ||||||
|  |  | ||||||
|  | 					cmdToRun.ExecuteHooks("error", opts) | ||||||
|  |  | ||||||
|  | 					// Notify failure | ||||||
|  | 					if list.NotifyConfig != nil { | ||||||
|  | 						notifyError(cmdLogger, msgTemps, list, cmdsRan, outStructArr, runErr, cmdToRun) | ||||||
| 					} | 					} | ||||||
|  |  | ||||||
| 			// Execute final hooks for every command | 					// Execute error hooks for the failed command | ||||||
| 			cmdToRun.ExecuteHooks("final", opts) | 					hasError = true | ||||||
|  | 					break | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
| 		// Send the final result for the list | 				if list.GetCommandOutputInNotificationsOnSuccess || cmdToRun.Output.InList { | ||||||
| 		if hasError { | 					outStructArr = append(outStructArr, outStruct{ | ||||||
| 			results <- CmdResult{CmdName: cmdsRan[len(cmdsRan)-1], ListName: list.Name, Error: fmt.Errorf("list execution failed")} | 						CmdName:     currentCmd, | ||||||
| 		} else { | 						CmdExecuted: currentCmd, | ||||||
| 			results <- CmdResult{CmdName: cmdsRan[len(cmdsRan)-1], ListName: list.Name, Error: nil} | 						Output:      outputArr, | ||||||
|  | 					}) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|  | 			if !hasError && list.NotifyConfig != nil && list.Notify.OnFailure { | ||||||
|  | 				notifySuccess(cmdLogger, msgTemps, list, cmdsRan, outStructArr) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if !hasError { | ||||||
|  | 				commandExecuted.ExecuteHooks("success", opts) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			commandExecuted.ExecuteHooks("final", opts) | ||||||
|  |  | ||||||
|  | 		} | ||||||
|  | 		results <- "done" | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // Helper to check if a command is in the list of executed commands | // func cmdListWorkerExecuteCommandsInParallel(msgTemps *msgTemplates, jobs <-chan *CmdList, hosts <-chan *Host, results chan<- string, opts *ConfigOpts) { | ||||||
| func cmdsRanContains(cmd string, cmdsRan []string) bool { | // 	opts.Logger.Info().Msg("Running commands in parallel") | ||||||
| 	for _, c := range cmdsRan { | // 	for list := range jobs { | ||||||
| 		if c == cmd { | // 		fieldsMap := map[string]interface{}{"list": list.Name} | ||||||
| 			return true | // 		var cmdLogger zerolog.Logger | ||||||
|  | // 		var commandExecuted *Command | ||||||
|  | // 		var cmdsRan []string | ||||||
|  | // 		var outStructArr []outStruct | ||||||
|  | // 		var hasError bool // Tracks if any command in the list failed | ||||||
|  |  | ||||||
|  | // 		for _, cmd := range list.Order { | ||||||
|  | // 			for host := range hosts { | ||||||
|  | // 				cmdToRun := opts.Cmds[cmd] | ||||||
|  | // 				if cmdToRun.Host != host.Host { | ||||||
|  | // 					cmdToRun.Host = host.Host | ||||||
|  | // 					cmdToRun.RemoteHost = host | ||||||
|  | // 				} | ||||||
|  | // 				commandExecuted = cmdToRun | ||||||
|  | // 				currentCmd := cmdToRun.Name | ||||||
|  | // 				fieldsMap["cmd"] = currentCmd | ||||||
|  | // 				cmdLogger = cmdToRun.GenerateLogger(opts) | ||||||
|  | // 				cmdLogger.Info().Fields(fieldsMap).Send() | ||||||
|  |  | ||||||
|  | // 				outputArr, runErr := cmdToRun.RunCmd(cmdLogger, opts) | ||||||
|  | // 				cmdsRan = append(cmdsRan, cmd) | ||||||
|  |  | ||||||
|  | // 				if runErr != nil { | ||||||
|  |  | ||||||
|  | // 					cmdLogger.Err(runErr).Send() | ||||||
|  |  | ||||||
|  | // 					cmdToRun.ExecuteHooks("error", opts) | ||||||
|  |  | ||||||
|  | // 					// Notify failure | ||||||
|  | // 					if list.NotifyConfig != nil { | ||||||
|  | // 						notifyError(cmdLogger, msgTemps, list, cmdsRan, outStructArr, runErr, cmdToRun) | ||||||
|  | // 					} | ||||||
|  |  | ||||||
|  | // 					// Execute error hooks for the failed command | ||||||
|  | // 					hasError = true | ||||||
|  | // 					break | ||||||
|  | // 				} | ||||||
|  |  | ||||||
|  | // 				if list.GetCommandOutputInNotificationsOnSuccess || cmdToRun.Output.InList { | ||||||
|  | // 					outStructArr = append(outStructArr, outStruct{ | ||||||
|  | // 						CmdName:     currentCmd, | ||||||
|  | // 						CmdExecuted: currentCmd, | ||||||
|  | // 						Output:      outputArr, | ||||||
|  | // 					}) | ||||||
|  | // 				} | ||||||
|  | // 			} | ||||||
|  |  | ||||||
|  | // 			if !hasError && list.NotifyConfig != nil && list.Notify.OnFailure { | ||||||
|  | // 				notifySuccess(cmdLogger, msgTemps, list, cmdsRan, outStructArr) | ||||||
|  | // 			} | ||||||
|  |  | ||||||
|  | // 			if !hasError { | ||||||
|  | // 				commandExecuted.ExecuteHooks("success", opts) | ||||||
|  | // 			} | ||||||
|  |  | ||||||
|  | // 			commandExecuted.ExecuteHooks("final", opts) | ||||||
|  |  | ||||||
|  | // 		} | ||||||
|  | // 		results <- "done" | ||||||
|  | // 	} | ||||||
|  | // } | ||||||
|  |  | ||||||
|  | func cmdListWorkerExecuteCommandsInParallel(msgTemps *msgTemplates, jobs <-chan *CmdList, hosts <-chan *Host, results chan<- string, opts *ConfigOpts) { | ||||||
|  | 	opts.Logger.Info().Msg("Running commands in parallel") | ||||||
|  | 	for list := range jobs { | ||||||
|  | 		fieldsMap := map[string]interface{}{"list": list.Name} | ||||||
|  | 		var cmdLogger zerolog.Logger | ||||||
|  | 		var commandExecuted *Command | ||||||
|  | 		var cmdsRan []string | ||||||
|  | 		var outStructArr []outStruct | ||||||
|  | 		var hasError bool // Tracks if any command in the list failed | ||||||
|  |  | ||||||
|  | 		var wg sync.WaitGroup | ||||||
|  | 		hostList := []*Host{} | ||||||
|  | 		for host := range hosts { | ||||||
|  | 			hostList = append(hostList, host) | ||||||
|  | 		} | ||||||
|  | 		println("Total hosts to run commands on:", len(hostList)) | ||||||
|  | 		println("Total commands to run:", len(list.Order)) | ||||||
|  |  | ||||||
|  | 		for _, cmd := range list.Order { | ||||||
|  | 			cmdsRan = append(cmdsRan, cmd) | ||||||
|  | 			println("Running cmd:", cmd, "on", len(hostList), "hosts") | ||||||
|  | 			outputChan := make(chan outStruct, len(hostList)) | ||||||
|  | 			errorChan := make(chan error, len(hostList)) | ||||||
|  | 			// cmdToRun := opts.Cmds[cmd] | ||||||
|  | 			origCmd := opts.Cmds[cmd] | ||||||
|  |  | ||||||
|  | 			for _, host := range hostList { | ||||||
|  | 				wg.Add(1) | ||||||
|  | 				cmdToRun := *origCmd // shallow copy | ||||||
|  | 				commandExecuted = origCmd | ||||||
|  | 				if cmdToRun.Host != host.Host { | ||||||
|  | 					cmdToRun.Host = host.Host | ||||||
|  | 					cmdToRun.RemoteHost = host | ||||||
|  | 				} | ||||||
|  | 				cmdLogger = cmdToRun.GenerateLogger(opts) | ||||||
|  | 				cmdLogger.Info().Fields(fieldsMap).Send() | ||||||
|  | 				print("Running cmd on: ", host.Host, "\n") | ||||||
|  |  | ||||||
|  | 				go func(cmd string, host *Host) { | ||||||
|  | 					defer wg.Done() | ||||||
|  | 					currentCmd := cmdToRun.Name | ||||||
|  | 					fieldsMap["cmd"] = currentCmd | ||||||
|  |  | ||||||
|  | 					outputArr, runErr := cmdToRun.RunCmd(cmdLogger, opts) | ||||||
|  | 					if runErr != nil { | ||||||
|  | 						cmdLogger.Err(runErr).Send() | ||||||
|  | 						cmdToRun.ExecuteHooks("error", opts) | ||||||
|  | 						errorChan <- runErr | ||||||
|  | 						return | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					if list.GetCommandOutputInNotificationsOnSuccess || cmdToRun.Output.InList { | ||||||
|  | 						outputChan <- outStruct{ | ||||||
|  | 							CmdName:     currentCmd, | ||||||
|  | 							CmdExecuted: currentCmd, | ||||||
|  | 							Output:      outputArr, | ||||||
| 						} | 						} | ||||||
| 					} | 					} | ||||||
| 	return false | 				}(cmd, host) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			wg.Wait() | ||||||
|  | 			close(outputChan) | ||||||
|  | 			close(errorChan) | ||||||
|  |  | ||||||
|  | 			for out := range outputChan { | ||||||
|  | 				outStructArr = append(outStructArr, out) | ||||||
|  | 			} | ||||||
|  | 			if len(errorChan) > 0 { | ||||||
|  | 				hasError = true | ||||||
|  | 				runErr := <-errorChan | ||||||
|  | 				if list.NotifyConfig != nil { | ||||||
|  | 					notifyError(cmdLogger, msgTemps, list, cmdsRan, outStructArr, runErr, commandExecuted) | ||||||
|  | 				} | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if !hasError && list.NotifyConfig != nil && list.Notify.OnFailure { | ||||||
|  | 				notifySuccess(cmdLogger, msgTemps, list, cmdsRan, outStructArr) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if !hasError { | ||||||
|  | 				commandExecuted.ExecuteHooks("success", opts) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 		} | ||||||
|  | 		commandExecuted.ExecuteHooks("final", opts) | ||||||
|  | 		results <- "done" | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // Helper to notify errors |  | ||||||
| func notifyError(logger zerolog.Logger, templates *msgTemplates, list *CmdList, cmdsRan []string, outStructArr []outStruct, err error, cmd *Command) { | 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, | ||||||
| @@ -332,7 +694,7 @@ func (opts *ConfigOpts) RunListConfig(cron string) { | |||||||
| 	} | 	} | ||||||
| 	configListsLen := len(opts.CmdConfigLists) | 	configListsLen := len(opts.CmdConfigLists) | ||||||
| 	listChan := make(chan *CmdList, configListsLen) | 	listChan := make(chan *CmdList, configListsLen) | ||||||
| 	results := make(chan CmdResult, configListsLen) | 	results := make(chan string, configListsLen) | ||||||
|  |  | ||||||
| 	// Start workers | 	// Start workers | ||||||
| 	for w := 1; w <= configListsLen; w++ { | 	for w := 1; w <= configListsLen; w++ { | ||||||
| @@ -352,14 +714,66 @@ func (opts *ConfigOpts) RunListConfig(cron string) { | |||||||
|  |  | ||||||
| 	// Process results | 	// Process results | ||||||
| 	for a := 1; a <= configListsLen; a++ { | 	for a := 1; a <= configListsLen; a++ { | ||||||
| 		result := <-results | 		<-results | ||||||
| 		opts.Logger.Debug().Msgf("Processing result for list %s, command %s", result.ListName, result.CmdName) |  | ||||||
|  |  | ||||||
| 		// Process final hooks for the list (already handled in worker) |  | ||||||
| 	} | 	} | ||||||
| 	opts.closeHostConnections() | 	opts.closeHostConnections() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (opts *ConfigOpts) ExecuteListOnHosts(lists []string, parallel bool) { | ||||||
|  |  | ||||||
|  | 	mTemps := &msgTemplates{ | ||||||
|  | 		err:     template.Must(template.New("error.txt").ParseFS(templates, "templates/error.txt")), | ||||||
|  | 		success: template.Must(template.New("success.txt").ParseFS(templates, "templates/success.txt")), | ||||||
|  | 	} | ||||||
|  | 	// for _, l := range opts.CmdConfigLists { | ||||||
|  | 	// 	if !slices.Contains(lists, l.Name) { | ||||||
|  | 	// 		delete(opts.CmdConfigLists, l.Name) | ||||||
|  | 	// 	} | ||||||
|  | 	// } | ||||||
|  | 	configListsLen := len(opts.CmdConfigLists) | ||||||
|  | 	listChan := make(chan *CmdList, configListsLen) | ||||||
|  | 	hostChan := make(chan *Host, len(opts.Hosts)) | ||||||
|  | 	results := make(chan string, configListsLen) | ||||||
|  |  | ||||||
|  | 	// Start workers | ||||||
|  | 	for w := 1; w <= configListsLen; w++ { | ||||||
|  | 		if parallel { | ||||||
|  | 			go cmdListWorkerExecuteCommandsInParallel(mTemps, listChan, hostChan, results, opts) | ||||||
|  | 		} else { | ||||||
|  | 			go cmdListWorkerWithHosts(mTemps, listChan, hostChan, results, opts) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Enqueue jobs | ||||||
|  | 	for listName, cmdConfig := range opts.CmdConfigLists { | ||||||
|  | 		if cmdConfig.Name == "" { | ||||||
|  | 			cmdConfig.Name = listName | ||||||
|  | 		} | ||||||
|  | 		listChan <- cmdConfig | ||||||
|  | 	} | ||||||
|  | 	for _, h := range opts.Hosts { | ||||||
|  | 		if h.isProxyHost { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		hostChan <- h | ||||||
|  | 		// for _, proxyHost := range h.ProxyHost { | ||||||
|  | 		// 	if proxyHost.isProxyHost { | ||||||
|  | 		// 		continue | ||||||
|  | 		// 	} | ||||||
|  | 		// 	hostChan <- proxyHost | ||||||
|  | 		// } | ||||||
|  | 	} | ||||||
|  | 	close(listChan) | ||||||
|  | 	close(hostChan) | ||||||
|  |  | ||||||
|  | 	// Process results | ||||||
|  | 	for a := 1; a <= configListsLen; a++ { | ||||||
|  | 		<-results | ||||||
|  | 	} | ||||||
|  | 	opts.closeHostConnections() | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
| func (opts *ConfigOpts) ExecuteCmds() { | func (opts *ConfigOpts) ExecuteCmds() { | ||||||
| 	for _, cmd := range opts.executeCmds { | 	for _, cmd := range opts.executeCmds { | ||||||
| 		cmdToRun := opts.Cmds[cmd] | 		cmdToRun := opts.Cmds[cmd] | ||||||
| @@ -367,10 +781,8 @@ func (opts *ConfigOpts) ExecuteCmds() { | |||||||
| 		_, runErr := cmdToRun.RunCmd(cmdLogger, opts) | 		_, runErr := cmdToRun.RunCmd(cmdLogger, opts) | ||||||
| 		if runErr != nil { | 		if runErr != nil { | ||||||
| 			opts.Logger.Err(runErr).Send() | 			opts.Logger.Err(runErr).Send() | ||||||
|  |  | ||||||
| 			cmdToRun.ExecuteHooks("error", opts) | 			cmdToRun.ExecuteHooks("error", opts) | ||||||
| 		} else { | 		} else { | ||||||
|  |  | ||||||
| 			cmdToRun.ExecuteHooks("success", opts) | 			cmdToRun.ExecuteHooks("success", opts) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @@ -378,7 +790,6 @@ func (opts *ConfigOpts) ExecuteCmds() { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	opts.closeHostConnections() | 	opts.closeHostConnections() | ||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func (c *ConfigOpts) closeHostConnections() { | func (c *ConfigOpts) closeHostConnections() { | ||||||
| @@ -426,26 +837,31 @@ func (cmd *Command) ExecuteHooks(hookType string, opts *ConfigOpts) { | |||||||
| 		for _, v := range cmd.Hooks.Error { | 		for _, v := range cmd.Hooks.Error { | ||||||
| 			errCmd := opts.Cmds[v] | 			errCmd := opts.Cmds[v] | ||||||
| 			cmdLogger := opts.Logger.With(). | 			cmdLogger := opts.Logger.With(). | ||||||
| 				Str("backy-cmd", v). | 				Str("backy-cmd", v).Str("hookType", "error"). | ||||||
| 				Logger() | 				Logger() | ||||||
| 			errCmd.RunCmd(cmdLogger, opts) | 			cmdLogger.Info().Msgf("Running error hook command %s", v) | ||||||
|  | 			// URGENT: Never returns | ||||||
|  | 			_, _ = errCmd.RunCmd(cmdLogger, opts) | ||||||
|  | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 	case "success": | 	case "success": | ||||||
| 		for _, v := range cmd.Hooks.Success { | 		for _, v := range cmd.Hooks.Success { | ||||||
| 			successCmd := opts.Cmds[v] | 			successCmd := opts.Cmds[v] | ||||||
| 			cmdLogger := opts.Logger.With(). | 			cmdLogger := opts.Logger.With(). | ||||||
| 				Str("backy-cmd", v). | 				Str("backy-cmd", v).Str("hookType", "success"). | ||||||
| 				Logger() | 				Logger() | ||||||
| 			successCmd.RunCmd(cmdLogger, opts) | 			cmdLogger.Info().Msgf("Running success hook command %s", v) | ||||||
|  | 			_, _ = successCmd.RunCmd(cmdLogger, opts) | ||||||
| 		} | 		} | ||||||
| 	case "final": | 	case "final": | ||||||
| 		for _, v := range cmd.Hooks.Final { | 		for _, v := range cmd.Hooks.Final { | ||||||
| 			finalCmd := opts.Cmds[v] | 			finalCmd := opts.Cmds[v] | ||||||
| 			cmdLogger := opts.Logger.With(). | 			cmdLogger := opts.Logger.With(). | ||||||
| 				Str("backy-cmd", v). | 				Str("backy-cmd", v).Str("hookType", "final"). | ||||||
| 				Logger() | 				Logger() | ||||||
| 			finalCmd.RunCmd(cmdLogger, opts) | 			cmdLogger.Info().Msgf("Running final hook command %s", v) | ||||||
|  | 			_, _ = finalCmd.RunCmd(cmdLogger, opts) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| @@ -455,30 +871,84 @@ func (cmd *Command) GenerateLogger(opts *ConfigOpts) zerolog.Logger { | |||||||
| 		Str("Backy-cmd", cmd.Name).Str("Host", "local machine"). | 		Str("Backy-cmd", cmd.Name).Str("Host", "local machine"). | ||||||
| 		Logger() | 		Logger() | ||||||
|  |  | ||||||
| 	if cmd.Host != nil { | 	if !IsHostLocal(cmd.Host) { | ||||||
| 		cmdLogger = opts.Logger.With(). | 		cmdLogger = opts.Logger.With(). | ||||||
| 			Str("Backy-cmd", cmd.Name).Str("Host", *cmd.Host). | 			Str("Backy-cmd", cmd.Name).Str("Host", cmd.Host). | ||||||
| 			Logger() | 			Logger() | ||||||
|  |  | ||||||
| 	} | 	} | ||||||
| 	return cmdLogger | 	return cmdLogger | ||||||
| } | } | ||||||
|  |  | ||||||
| func (opts *ConfigOpts) ExecCmdsSSH(cmdList []string, hostsList []string) { | func (opts *ConfigOpts) ExecCmdsOnHosts(cmdList []string, hostsList []string) { | ||||||
| 	// Iterate over hosts and exec commands | 	// Iterate over hosts and exec commands | ||||||
| 	for _, h := range hostsList { | 	for _, h := range hostsList { | ||||||
| 		host := opts.Hosts[h] | 		host := opts.Hosts[h] | ||||||
| 		for _, c := range cmdList { | 		for _, c := range cmdList { | ||||||
| 			cmd := opts.Cmds[c] | 			cmd := opts.Cmds[c] | ||||||
| 			cmd.RemoteHost = host | 			cmd.RemoteHost = host | ||||||
| 			cmd.Host = &host.Host | 			cmd.Host = h | ||||||
|  | 			if IsHostLocal(h) { | ||||||
|  | 				_, err := cmd.RunCmd(cmd.GenerateLogger(opts), opts) | ||||||
|  | 				if err != nil { | ||||||
|  | 					opts.Logger.Err(err).Str("host", h).Str("cmd", c).Send() | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  |  | ||||||
|  | 				cmd.Host = host.Host | ||||||
| 				opts.Logger.Info().Str("host", h).Str("cmd", c).Send() | 				opts.Logger.Info().Str("host", h).Str("cmd", c).Send() | ||||||
| 			_, err := cmd.RunCmdSSH(cmd.GenerateLogger(opts), opts) | 				_, err := cmd.RunCmdOnHost(cmd.GenerateLogger(opts), opts) | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
| 					opts.Logger.Err(err).Str("host", h).Str("cmd", c).Send() | 					opts.Logger.Err(err).Str("host", h).Str("cmd", c).Send() | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (opts *ConfigOpts) ExecCmdsOnHostsInParallel(cmdList []string, hostsList []string) { | ||||||
|  | 	opts.Logger.Info().Msg("Executing commands in parallel on hosts") | ||||||
|  | 	// Iterate over hosts and exec commands | ||||||
|  | 	for _, c := range cmdList { | ||||||
|  | 		for _, h := range hostsList { | ||||||
|  | 			host := opts.Hosts[h] | ||||||
|  | 			cmd := opts.Cmds[c] | ||||||
|  | 			cmd.RemoteHost = host | ||||||
|  | 			cmd.Host = h | ||||||
|  | 			if IsHostLocal(h) { | ||||||
|  | 				_, err := cmd.RunCmd(cmd.GenerateLogger(opts), opts) | ||||||
|  | 				if err != nil { | ||||||
|  | 					opts.Logger.Err(err).Str("host", h).Str("cmd", c).Send() | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  |  | ||||||
|  | 				cmd.Host = host.Host | ||||||
|  | 				opts.Logger.Info().Str("host", h).Str("cmd", c).Send() | ||||||
|  | 				_, err := cmd.RunCmdOnHost(cmd.GenerateLogger(opts), opts) | ||||||
|  | 				if err != nil { | ||||||
|  | 					opts.Logger.Err(err).Str("host", h).Str("cmd", c).Send() | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func logCommandOutput(command *Command, cmdOutBuf bytes.Buffer, cmdCtxLogger zerolog.Logger, outputArr []string) []string { | ||||||
|  |  | ||||||
|  | 	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) | ||||||
|  | 		} | ||||||
|  | 		if command.Output.ToLog { | ||||||
|  | 			cmdCtxLogger.Info().Fields(outMap).Send() | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return outputArr | ||||||
| } | } | ||||||
|  |  | ||||||
| // func executeUserCommands() []string { | // func executeUserCommands() []string { | ||||||
|   | |||||||
							
								
								
									
										83
									
								
								pkg/backy/backy_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								pkg/backy/backy_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | |||||||
|  | package backy | ||||||
|  |  | ||||||
|  | // import ( | ||||||
|  | // 	"context" | ||||||
|  | // 	"fmt" | ||||||
|  | // 	"io" | ||||||
|  | // 	"log" | ||||||
|  | // 	"testing" | ||||||
|  |  | ||||||
|  | // 	"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman" | ||||||
|  | // 	packagemanagercommon "git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/common" | ||||||
|  | // 	"github.com/stretchr/testify/assert" | ||||||
|  | // 	"github.com/stretchr/testify/require" | ||||||
|  |  | ||||||
|  | // 	"github.com/testcontainers/testcontainers-go" | ||||||
|  | // ) | ||||||
|  |  | ||||||
|  | // // TestConfigOptions tests the configuration options for the backy package. | ||||||
|  | // func Test_ErrorHook(t *testing.T) { | ||||||
|  |  | ||||||
|  | // 	configFile := "../../tests/ErrorHook.yml" | ||||||
|  | // 	logFile := "ErrorHook.log" | ||||||
|  | // 	backyConfigOptions := NewConfigOptions(configFile, SetLogFile(logFile)) | ||||||
|  | // 	backyConfigOptions.InitConfig() | ||||||
|  | // 	backyConfigOptions.ParseConfigurationFile() | ||||||
|  | // 	backyConfigOptions.RunListConfig("") | ||||||
|  |  | ||||||
|  | // } | ||||||
|  |  | ||||||
|  | // func TestSettingCommandInfoPackageCommandDnf(t *testing.T) { | ||||||
|  |  | ||||||
|  | // 	packagecommand := &Command{ | ||||||
|  | // 		Type:             PackageCommandType, | ||||||
|  | // 		PackageManager:   "dnf", | ||||||
|  | // 		Shell:            "zsh", | ||||||
|  | // 		PackageOperation: PackageOperationCheckVersion, | ||||||
|  | // 		Packages:         []packagemanagercommon.Package{{Name: "docker-ce"}}, | ||||||
|  | // 	} | ||||||
|  | // 	dnfPackage, _ := pkgman.PackageManagerFactory("dnf", pkgman.WithoutAuth()) | ||||||
|  |  | ||||||
|  | // 	packagecommand.pkgMan = dnfPackage | ||||||
|  | // 	PackageCommand := getCommandTypeAndSetCommandInfo(packagecommand) | ||||||
|  |  | ||||||
|  | // 	assert.Equal(t, "dnf", PackageCommand.Cmd) | ||||||
|  |  | ||||||
|  | // } | ||||||
|  |  | ||||||
|  | // func TestWithDockerFile(t *testing.T) { | ||||||
|  | // 	ctx := context.Background() | ||||||
|  |  | ||||||
|  | // 	docker, err := testcontainers.Run(ctx, "", | ||||||
|  | // 		testcontainers.WithDockerfile(testcontainers.FromDockerfile{ | ||||||
|  | // 			Context:    "../../tests/docker", | ||||||
|  | // 			Dockerfile: "Dockerfile", | ||||||
|  | // 			KeepImage:  false, | ||||||
|  | // 			// BuildOptionsModifier: func(buildOptions *types.ImageBuildOptions) { | ||||||
|  | // 			// 	buildOptions.Target = "target2" | ||||||
|  | // 			// }, | ||||||
|  | // 		}), | ||||||
|  | // 	) | ||||||
|  | // 	// docker. | ||||||
|  |  | ||||||
|  | // 	if err != nil { | ||||||
|  | // 		log.Printf("failed to start container: %v", err) | ||||||
|  | // 		return | ||||||
|  | // 	} | ||||||
|  |  | ||||||
|  | // 	r, err := docker.Logs(ctx) | ||||||
|  | // 	if err != nil { | ||||||
|  | // 		log.Printf("failed to get logs: %v", err) | ||||||
|  | // 		return | ||||||
|  | // 	} | ||||||
|  |  | ||||||
|  | // 	logs, err := io.ReadAll(r) | ||||||
|  | // 	if err != nil { | ||||||
|  | // 		log.Printf("failed to read logs: %v", err) | ||||||
|  | // 		return | ||||||
|  | // 	} | ||||||
|  |  | ||||||
|  | // 	fmt.Println(string(logs)) | ||||||
|  |  | ||||||
|  | // 	require.NoError(t, err) | ||||||
|  | // } | ||||||
| @@ -25,29 +25,29 @@ func (i CommandType) String() string { | |||||||
| // Re-run the stringer command to generate them again. | // Re-run the stringer command to generate them again. | ||||||
| func _CommandTypeNoOp() { | func _CommandTypeNoOp() { | ||||||
| 	var x [1]struct{} | 	var x [1]struct{} | ||||||
| 	_ = x[DefaultCT-(0)] | 	_ = x[DefaultCommandType-(0)] | ||||||
| 	_ = x[ScriptCT-(1)] | 	_ = x[ScriptCommandType-(1)] | ||||||
| 	_ = x[ScriptFileCT-(2)] | 	_ = x[ScriptFileCommandType-(2)] | ||||||
| 	_ = x[RemoteScriptCT-(3)] | 	_ = x[RemoteScriptCommandType-(3)] | ||||||
| 	_ = x[PackageCT-(4)] | 	_ = x[PackageCommandType-(4)] | ||||||
| 	_ = x[UserCT-(5)] | 	_ = x[UserCommandType-(5)] | ||||||
| } | } | ||||||
|  |  | ||||||
| var _CommandTypeValues = []CommandType{DefaultCT, ScriptCT, ScriptFileCT, RemoteScriptCT, PackageCT, UserCT} | var _CommandTypeValues = []CommandType{DefaultCommandType, ScriptCommandType, ScriptFileCommandType, RemoteScriptCommandType, PackageCommandType, UserCommandType} | ||||||
|  |  | ||||||
| var _CommandTypeNameToValueMap = map[string]CommandType{ | var _CommandTypeNameToValueMap = map[string]CommandType{ | ||||||
| 	_CommandTypeName[0:0]:        DefaultCT, | 	_CommandTypeName[0:0]:        DefaultCommandType, | ||||||
| 	_CommandTypeLowerName[0:0]:   DefaultCT, | 	_CommandTypeLowerName[0:0]:   DefaultCommandType, | ||||||
| 	_CommandTypeName[0:6]:        ScriptCT, | 	_CommandTypeName[0:6]:        ScriptCommandType, | ||||||
| 	_CommandTypeLowerName[0:6]:   ScriptCT, | 	_CommandTypeLowerName[0:6]:   ScriptCommandType, | ||||||
| 	_CommandTypeName[6:16]:       ScriptFileCT, | 	_CommandTypeName[6:16]:       ScriptFileCommandType, | ||||||
| 	_CommandTypeLowerName[6:16]:  ScriptFileCT, | 	_CommandTypeLowerName[6:16]:  ScriptFileCommandType, | ||||||
| 	_CommandTypeName[16:28]:      RemoteScriptCT, | 	_CommandTypeName[16:28]:      RemoteScriptCommandType, | ||||||
| 	_CommandTypeLowerName[16:28]: RemoteScriptCT, | 	_CommandTypeLowerName[16:28]: RemoteScriptCommandType, | ||||||
| 	_CommandTypeName[28:35]:      PackageCT, | 	_CommandTypeName[28:35]:      PackageCommandType, | ||||||
| 	_CommandTypeLowerName[28:35]: PackageCT, | 	_CommandTypeLowerName[28:35]: PackageCommandType, | ||||||
| 	_CommandTypeName[35:39]:      UserCT, | 	_CommandTypeName[35:39]:      UserCommandType, | ||||||
| 	_CommandTypeLowerName[35:39]: UserCT, | 	_CommandTypeLowerName[35:39]: UserCommandType, | ||||||
| } | } | ||||||
|  |  | ||||||
| var _CommandTypeNames = []string{ | var _CommandTypeNames = []string{ | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| package backy | package backy | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" |  | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| @@ -22,10 +21,13 @@ import ( | |||||||
| 	"github.com/rs/zerolog" | 	"github.com/rs/zerolog" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const macroStart string = "%{" | const ( | ||||||
| const macroEnd string = "}%" | 	externDirectiveStart      string = "%{" | ||||||
| const envMacroStart string = "%{env:" | 	externDirectiveEnd        string = "}%" | ||||||
| const vaultMacroStart string = "%{vault:" | 	externFileDirectiveStart  string = "%{file:" | ||||||
|  | 	envExternDirectiveStart   string = "%{env:" | ||||||
|  | 	vaultExternDirectiveStart string = "%{vault:" | ||||||
|  | ) | ||||||
|  |  | ||||||
| func (opts *ConfigOpts) InitConfig() { | func (opts *ConfigOpts) InitConfig() { | ||||||
| 	var err error | 	var err error | ||||||
| @@ -78,30 +80,117 @@ func (opts *ConfigOpts) InitConfig() { | |||||||
| 		logging.ExitWithMSG(fmt.Sprintf("error initializing cache: %v", err), 1, nil) | 		logging.ExitWithMSG(fmt.Sprintf("error initializing cache: %v", err), 1, nil) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	fetcher, err := remotefetcher.NewRemoteFetcher(opts.ConfigFilePath, opts.Cache) |  | ||||||
|  |  | ||||||
| 	if isRemoteURL(opts.ConfigFilePath) { | 	if isRemoteURL(opts.ConfigFilePath) { | ||||||
| 		p, _ := getRemoteDir(opts.ConfigFilePath) | 		p, _ := getRemoteDir(opts.ConfigFilePath) | ||||||
| 		opts.ConfigDir = p | 		opts.ConfigDir = p | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	fetcher, err := remotefetcher.NewRemoteFetcher(opts.ConfigFilePath, opts.Cache) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		logging.ExitWithMSG(fmt.Sprintf("error initializing config fetcher: %v", err), 1, nil) | 		logging.ExitWithMSG(fmt.Sprintf("error initializing config fetcher: %v", err), 1, nil) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if opts.ConfigFilePath != "" { | 	if opts.ConfigFilePath != "" { | ||||||
| 		loadConfigFile(fetcher, opts.ConfigFilePath, backyKoanf, opts) | 		loadConfigFile(fetcher, opts.ConfigFilePath, backyKoanf, opts) | ||||||
| 	} else { | 	} else { | ||||||
| 		loadDefaultConfigFiles(fetcher, configFiles, backyKoanf, opts) | 		loadDefaultConfigFiles(fetcher, configFiles, backyKoanf, opts) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	opts.koanf = backyKoanf | 	opts.koanf = backyKoanf | ||||||
| } | } | ||||||
|  |  | ||||||
| func loadConfigFile(fetcher remotefetcher.RemoteFetcher, filePath string, k *koanf.Koanf, opts *ConfigOpts) { | func (opts *ConfigOpts) ParseConfigurationFile() *ConfigOpts { | ||||||
| 	data, err := fetcher.Fetch(filePath) | 	setTerminalEnv() | ||||||
| 	if err != nil { |  | ||||||
| 		logging.ExitWithMSG(fmt.Sprintf("Could not fetch config file %s: %v", filePath, err), 1, nil) | 	backyKoanf := opts.koanf | ||||||
|  |  | ||||||
|  | 	if backyKoanf.Exists("variables") { | ||||||
|  | 		unmarshalConfigIntoStruct(backyKoanf, "variables", &opts.Vars, opts.Logger) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := k.Load(rawbytes.Provider(data), yaml.Parser()); err != nil { | 	getConfigDir(opts) | ||||||
|  |  | ||||||
|  | 	opts.loadEnv() | ||||||
|  |  | ||||||
|  | 	if backyKoanf.Bool(getNestedConfig("logging", "cmd-std-out")) { | ||||||
|  | 		os.Setenv("BACKY_CMDSTDOUT", "enabled") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// override the default value of cmd-std-out if flag is set | ||||||
|  | 	if opts.CmdStdOut { | ||||||
|  | 		os.Setenv("BACKY_CMDSTDOUT", "enabled") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	CheckConfigValues(backyKoanf, opts.ConfigFilePath) | ||||||
|  |  | ||||||
|  | 	validateExecCommandsFromCLI(backyKoanf, opts) | ||||||
|  |  | ||||||
|  | 	setLoggingOptions(backyKoanf, opts) | ||||||
|  |  | ||||||
|  | 	log := setupLogger(opts) | ||||||
|  | 	opts.Logger = log | ||||||
|  |  | ||||||
|  | 	hostsFetcher, err := remotefetcher.NewRemoteFetcher(opts.HostsFilePath, opts.Cache) | ||||||
|  | 	opts.Logger.Info().Str("hosts file", opts.HostsFilePath).Send() | ||||||
|  | 	if err != nil { | ||||||
|  | 		logging.ExitWithMSG(fmt.Sprintf("error initializing config fetcher: %v", err), 1, nil) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var hostKoanf = koanf.New(".") | ||||||
|  | 	if opts.HostsFilePath != "" { | ||||||
|  | 		loadConfigFile(hostsFetcher, opts.HostsFilePath, hostKoanf, opts) | ||||||
|  | 		unmarshalConfigIntoStruct(hostKoanf, "hosts", &opts.Hosts, opts.Logger) | ||||||
|  | 	} else { | ||||||
|  | 		unmarshalConfigIntoStruct(backyKoanf, "hosts", &opts.Hosts, opts.Logger) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	log.Info().Str("config file", opts.ConfigFilePath).Send() | ||||||
|  |  | ||||||
|  | 	if err := opts.initializeVault(); err != nil { | ||||||
|  | 		log.Err(err).Send() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	unmarshalConfigIntoStruct(backyKoanf, "commands", &opts.Cmds, opts.Logger) | ||||||
|  |  | ||||||
|  | 	getCommandEnvironments(opts) | ||||||
|  |  | ||||||
|  | 	getHostConfigs(opts) | ||||||
|  |  | ||||||
|  | 	for k, v := range opts.Vars { | ||||||
|  | 		v = getExternalConfigDirectiveValue(v, opts, AllowedExternalDirectiveAll) | ||||||
|  | 		opts.Vars[k] = v | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	loadCommandLists(opts, backyKoanf) | ||||||
|  |  | ||||||
|  | 	validateCommandLists(opts) | ||||||
|  |  | ||||||
|  | 	if opts.cronEnabled && len(opts.CmdConfigLists) == 0 { | ||||||
|  | 		logging.ExitWithMSG("No cron fields detected in any command lists", 1, nil) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := processCmds(opts); err != nil { | ||||||
|  | 		logging.ExitWithMSG(err.Error(), 1, &opts.Logger) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	filterExecuteLists(opts) | ||||||
|  |  | ||||||
|  | 	if backyKoanf.Exists("notifications") { | ||||||
|  | 		unmarshalConfigIntoStruct(backyKoanf, "notifications", &opts.NotificationConf, opts.Logger) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	opts.SetupNotify() | ||||||
|  |  | ||||||
|  | 	return opts | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func loadConfigFile(fetcher remotefetcher.RemoteFetcher, filePath string, koanfConfigParser *koanf.Koanf, opts *ConfigOpts) { | ||||||
|  | 	data, err := fetcher.Fetch(filePath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logging.ExitWithMSG(generateFileFetchErrorString(filePath, "config", err), 1, nil) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := koanfConfigParser.Load(rawbytes.Provider(data), yaml.Parser()); err != nil { | ||||||
| 		logging.ExitWithMSG(fmt.Sprintf("error loading config: %v", err), 1, &opts.Logger) | 		logging.ExitWithMSG(fmt.Sprintf("error loading config: %v", err), 1, &opts.Logger) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| @@ -130,68 +219,6 @@ func loadDefaultConfigFiles(fetcher remotefetcher.RemoteFetcher, configFiles []s | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (opts *ConfigOpts) ReadConfig() *ConfigOpts { |  | ||||||
| 	setTerminalEnv() |  | ||||||
|  |  | ||||||
| 	backyKoanf := opts.koanf |  | ||||||
|  |  | ||||||
| 	opts.loadEnv() |  | ||||||
|  |  | ||||||
| 	if backyKoanf.Bool(getNestedConfig("logging", "cmd-std-out")) { |  | ||||||
| 		os.Setenv("BACKY_CMDSTDOUT", "enabled") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// override the default value of cmd-std-out if flag is set |  | ||||||
| 	if opts.CmdStdOut { |  | ||||||
| 		os.Setenv("BACKY_CMDSTDOUT", "enabled") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	CheckConfigValues(backyKoanf, opts.ConfigFilePath) |  | ||||||
|  |  | ||||||
| 	validateCommands(backyKoanf, opts) |  | ||||||
|  |  | ||||||
| 	setLoggingOptions(backyKoanf, opts) |  | ||||||
|  |  | ||||||
| 	log := setupLogger(opts) |  | ||||||
| 	opts.Logger = log |  | ||||||
|  |  | ||||||
| 	log.Info().Str("config file", opts.ConfigFilePath).Send() |  | ||||||
|  |  | ||||||
| 	unmarshalConfig(backyKoanf, "commands", &opts.Cmds, opts.Logger) |  | ||||||
|  |  | ||||||
| 	validateCommandEnvironments(opts) |  | ||||||
|  |  | ||||||
| 	unmarshalConfig(backyKoanf, "hosts", &opts.Hosts, opts.Logger) |  | ||||||
|  |  | ||||||
| 	resolveHostConfigs(opts) |  | ||||||
|  |  | ||||||
| 	loadCommandLists(opts, backyKoanf) |  | ||||||
|  |  | ||||||
| 	validateCommandLists(opts) |  | ||||||
|  |  | ||||||
| 	if opts.cronEnabled && len(opts.CmdConfigLists) == 0 { |  | ||||||
| 		logging.ExitWithMSG("No cron fields detected in any command lists", 1, nil) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if err := processCmds(opts); err != nil { |  | ||||||
| 		logging.ExitWithMSG(err.Error(), 1, &opts.Logger) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	filterExecuteLists(opts) |  | ||||||
|  |  | ||||||
| 	if backyKoanf.Exists("notifications") { |  | ||||||
| 		unmarshalConfig(backyKoanf, "notifications", &opts.NotificationConf, opts.Logger) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	opts.SetupNotify() |  | ||||||
|  |  | ||||||
| 	if err := opts.setupVault(); err != nil { |  | ||||||
| 		log.Err(err).Send() |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return opts |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func setTerminalEnv() { | func setTerminalEnv() { | ||||||
| 	if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) { | 	if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) { | ||||||
| 		os.Setenv("BACKY_TERM", "enabled") | 		os.Setenv("BACKY_TERM", "enabled") | ||||||
| @@ -200,7 +227,7 @@ func setTerminalEnv() { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func validateCommands(k *koanf.Koanf, opts *ConfigOpts) { | func validateExecCommandsFromCLI(k *koanf.Koanf, opts *ConfigOpts) { | ||||||
| 	for _, c := range opts.executeCmds { | 	for _, c := range opts.executeCmds { | ||||||
| 		if !k.Exists(getCmdFromConfig(c)) { | 		if !k.Exists(getCmdFromConfig(c)) { | ||||||
| 			logging.ExitWithMSG(fmt.Sprintf("command %s is not in config file %s", c, opts.ConfigFilePath), 1, nil) | 			logging.ExitWithMSG(fmt.Sprintf("command %s is not in config file %s", c, opts.ConfigFilePath), 1, nil) | ||||||
| @@ -208,23 +235,23 @@ func validateCommands(k *koanf.Koanf, opts *ConfigOpts) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func setLoggingOptions(k *koanf.Koanf, opts *ConfigOpts) { | func setLoggingOptions(backyKoanf *koanf.Koanf, opts *ConfigOpts) { | ||||||
| 	isLoggingVerbose := k.Bool(getLoggingKeyFromConfig("verbose")) | 	isVerboseLoggingSetInConfig := backyKoanf.Bool(getLoggingKeyFromConfig("verbose")) | ||||||
|  |  | ||||||
| 	// if log file is set in config file and not set on command line, use "./backy.log" | 	// if log file is set in config file and not set on command line, use "./backy.log" | ||||||
| 	logFile := "./backy.log" | 	logFile := "./backy.log" | ||||||
| 	if opts.LogFilePath == "" && k.Exists(getLoggingKeyFromConfig("file")) { | 	if opts.LogFilePath == "" && backyKoanf.Exists(getLoggingKeyFromConfig("file")) { | ||||||
| 		logFile = k.String(getLoggingKeyFromConfig("file")) | 		logFile = backyKoanf.String(getLoggingKeyFromConfig("file")) | ||||||
| 		opts.LogFilePath = logFile | 		opts.LogFilePath = logFile | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	zerolog.SetGlobalLevel(zerolog.InfoLevel) | 	zerolog.SetGlobalLevel(zerolog.InfoLevel) | ||||||
| 	if isLoggingVerbose { | 	if isVerboseLoggingSetInConfig { | ||||||
| 		zerolog.SetGlobalLevel(zerolog.DebugLevel) | 		zerolog.SetGlobalLevel(zerolog.DebugLevel) | ||||||
| 		os.Setenv("BACKY_LOGLEVEL", fmt.Sprintf("%v", zerolog.GlobalLevel())) | 		os.Setenv("BACKY_LOGLEVEL", fmt.Sprintf("%v", zerolog.GlobalLevel())) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if k.Bool(getLoggingKeyFromConfig("console-disabled")) { | 	if backyKoanf.Bool(getLoggingKeyFromConfig("console-disabled")) { | ||||||
| 		os.Setenv("BACKY_CONSOLE_LOGGING", "") | 		os.Setenv("BACKY_CONSOLE_LOGGING", "") | ||||||
| 	} else { | 	} else { | ||||||
| 		os.Setenv("BACKY_CONSOLE_LOGGING", "enabled") | 		os.Setenv("BACKY_CONSOLE_LOGGING", "enabled") | ||||||
| @@ -236,34 +263,37 @@ func setupLogger(opts *ConfigOpts) zerolog.Logger { | |||||||
| 	return zerolog.New(writers).With().Timestamp().Logger() | 	return zerolog.New(writers).With().Timestamp().Logger() | ||||||
| } | } | ||||||
|  |  | ||||||
| func unmarshalConfig(k *koanf.Koanf, key string, target interface{}, log zerolog.Logger) { | func unmarshalConfigIntoStruct(k *koanf.Koanf, key string, target interface{}, log zerolog.Logger) { | ||||||
| 	if err := k.UnmarshalWithConf(key, target, koanf.UnmarshalConf{Tag: "yaml"}); err != nil { | 	if err := k.UnmarshalWithConf(key, target, koanf.UnmarshalConf{Tag: "yaml"}); err != nil { | ||||||
| 		logging.ExitWithMSG(fmt.Sprintf("error unmarshalling key %s into struct: %v", key, err), 1, &log) | 		logging.ExitWithMSG(fmt.Sprintf("error unmarshaling key %s into struct: %v", key, err), 1, &log) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func validateCommandEnvironments(opts *ConfigOpts) { | func getCommandEnvironments(opts *ConfigOpts) { | ||||||
| 	for cmdName, cmdConf := range opts.Cmds { | 	for cmdName, cmdConf := range opts.Cmds { | ||||||
|  | 		if cmdConf.Env == "" { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		opts.Logger.Debug().Str("env file", cmdConf.Env).Str("cmd", cmdName).Send() | ||||||
| 		if err := testFile(cmdConf.Env); err != nil { | 		if err := testFile(cmdConf.Env); err != nil { | ||||||
| 			opts.Logger.Info().Str("cmd", cmdName).Err(err).Send() | 			logging.ExitWithMSG("Could not open file"+cmdConf.Env+": "+err.Error(), 1, &opts.Logger) | ||||||
| 			os.Exit(1) |  | ||||||
| 		} | 		} | ||||||
| 		expandEnvVars(opts.backyEnv, cmdConf.Environment) | 		expandEnvVars(opts.backyEnv, cmdConf.Environment) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func resolveHostConfigs(opts *ConfigOpts) { | func getHostConfigs(opts *ConfigOpts) { | ||||||
| 	for hostConfigName, host := range opts.Hosts { | 	for hostConfigName, host := range opts.Hosts { | ||||||
| 		if host.Host == "" { | 		if host.Host == "" { | ||||||
| 			host.Host = hostConfigName | 			host.Host = hostConfigName | ||||||
| 		} | 		} | ||||||
| 		if host.ProxyJump != "" { | 		if host.ProxyJump != "" { | ||||||
| 			resolveProxyHosts(host, opts) | 			getProxyHosts(host, opts) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func resolveProxyHosts(host *Host, opts *ConfigOpts) { | func getProxyHosts(host *Host, opts *ConfigOpts) { | ||||||
| 	proxyHosts := strings.Split(host.ProxyJump, ",") | 	proxyHosts := strings.Split(host.ProxyJump, ",") | ||||||
| 	for _, h := range proxyHosts { | 	for _, h := range proxyHosts { | ||||||
| 		proxyHost, defined := opts.Hosts[h] | 		proxyHost, defined := opts.Hosts[h] | ||||||
| @@ -275,16 +305,22 @@ func resolveProxyHosts(host *Host, opts *ConfigOpts) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func getConfigDir(opts *ConfigOpts) { | ||||||
|  | 	if isRemoteURL(opts.ConfigFilePath) { | ||||||
|  | 		p, _ := getRemoteDir(opts.ConfigFilePath) | ||||||
|  | 		opts.ConfigDir = p | ||||||
|  | 	} else { | ||||||
|  | 		opts.ConfigDir = path.Dir(opts.ConfigFilePath) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| func loadCommandLists(opts *ConfigOpts, backyKoanf *koanf.Koanf) { | func loadCommandLists(opts *ConfigOpts, backyKoanf *koanf.Koanf) { | ||||||
| 	var listConfigFiles []string | 	var listConfigFiles []string | ||||||
| 	var u *url.URL | 	var u *url.URL | ||||||
| 	var p string | 	var p string | ||||||
| 	// if config file is remote, use the directory of the remote file |  | ||||||
| 	if isRemoteURL(opts.ConfigFilePath) { | 	if isRemoteURL(opts.ConfigFilePath) { | ||||||
| 		p, u = getRemoteDir(opts.ConfigFilePath) | 		p, u = getRemoteDir(opts.ConfigFilePath) | ||||||
| 		opts.ConfigDir = p | 		opts.ConfigDir = p | ||||||
| 		println(p) |  | ||||||
| 		// // Still use local list files if a remote config file is used, but use them last |  | ||||||
| 		listConfigFiles = []string{u.JoinPath("lists.yml").String(), u.JoinPath("lists.yaml").String()} | 		listConfigFiles = []string{u.JoinPath("lists.yml").String(), u.JoinPath("lists.yaml").String()} | ||||||
| 	} else { | 	} else { | ||||||
| 		opts.ConfigDir = path.Dir(opts.ConfigFilePath) | 		opts.ConfigDir = path.Dir(opts.ConfigFilePath) | ||||||
| @@ -297,17 +333,20 @@ func loadCommandLists(opts *ConfigOpts, backyKoanf *koanf.Koanf) { | |||||||
|  |  | ||||||
| 	listsConfig := koanf.New(".") | 	listsConfig := koanf.New(".") | ||||||
|  |  | ||||||
|  | 	if backyKoanf.Exists("cmdLists") { | ||||||
|  | 		if backyKoanf.Exists("cmdLists.file") { | ||||||
|  | 			loadCmdListsFile(backyKoanf, listsConfig, opts) | ||||||
|  | 		} else { | ||||||
|  | 			unmarshalConfigIntoStruct(backyKoanf, "cmdLists", &opts.CmdConfigLists, opts.Logger) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if opts.CmdConfigLists == nil { | ||||||
| 		for _, l := range listConfigFiles { | 		for _, l := range listConfigFiles { | ||||||
| 			if loadListConfigFile(l, listsConfig, opts) { | 			if loadListConfigFile(l, listsConfig, opts) { | ||||||
| 				break | 				break | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 	if backyKoanf.Exists("cmdLists") { |  | ||||||
| 		unmarshalConfig(backyKoanf, "cmdLists", &opts.CmdConfigLists, opts.Logger) |  | ||||||
| 		if backyKoanf.Exists("cmdLists.file") { |  | ||||||
| 			loadCmdListsFile(backyKoanf, listsConfig, opts) |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -346,7 +385,7 @@ func loadListConfigFile(filePath string, k *koanf.Koanf, opts *ConfigOpts) bool | |||||||
| 		return false | 		return false | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	unmarshalConfig(k, "cmdLists", &opts.CmdConfigLists, opts.Logger) | 	unmarshalConfigIntoStruct(k, "cmdLists", &opts.CmdConfigLists, opts.Logger) | ||||||
| 	keyNotSupported("cmd-lists", "cmdLists", k, opts, true) | 	keyNotSupported("cmd-lists", "cmdLists", k, opts, true) | ||||||
| 	opts.CmdListFile = filePath | 	opts.CmdListFile = filePath | ||||||
| 	return true | 	return true | ||||||
| @@ -355,6 +394,7 @@ func loadListConfigFile(filePath string, k *koanf.Koanf, opts *ConfigOpts) bool | |||||||
| func loadCmdListsFile(backyKoanf *koanf.Koanf, listsConfig *koanf.Koanf, opts *ConfigOpts) { | func loadCmdListsFile(backyKoanf *koanf.Koanf, listsConfig *koanf.Koanf, opts *ConfigOpts) { | ||||||
| 	opts.CmdListFile = strings.TrimSpace(backyKoanf.String("cmdLists.file")) | 	opts.CmdListFile = strings.TrimSpace(backyKoanf.String("cmdLists.file")) | ||||||
| 	if !path.IsAbs(opts.CmdListFile) { | 	if !path.IsAbs(opts.CmdListFile) { | ||||||
|  | 		// TODO: Needs testing - might cause undefined/unexpected behavior if remote config path is used | ||||||
| 		opts.CmdListFile = path.Join(path.Dir(opts.ConfigFilePath), opts.CmdListFile) | 		opts.CmdListFile = path.Join(path.Dir(opts.ConfigFilePath), opts.CmdListFile) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -366,7 +406,7 @@ func loadCmdListsFile(backyKoanf *koanf.Koanf, listsConfig *koanf.Koanf, opts *C | |||||||
|  |  | ||||||
| 	data, err := fetcher.Fetch(opts.CmdListFile) | 	data, err := fetcher.Fetch(opts.CmdListFile) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		logging.ExitWithMSG(fmt.Sprintf("Could not fetch config file %s: %v", opts.CmdListFile, err), 1, nil) | 		logging.ExitWithMSG(generateFileFetchErrorString(opts.CmdListFile, "list config", err), 1, nil) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := listsConfig.Load(rawbytes.Provider(data), yaml.Parser()); err != nil { | 	if err := listsConfig.Load(rawbytes.Provider(data), yaml.Parser()); err != nil { | ||||||
| @@ -374,10 +414,14 @@ func loadCmdListsFile(backyKoanf *koanf.Koanf, listsConfig *koanf.Koanf, opts *C | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	keyNotSupported("cmd-lists", "cmdLists", listsConfig, opts, true) | 	keyNotSupported("cmd-lists", "cmdLists", listsConfig, opts, true) | ||||||
| 	unmarshalConfig(listsConfig, "cmdLists", &opts.CmdConfigLists, opts.Logger) | 	unmarshalConfigIntoStruct(listsConfig, "cmdLists", &opts.CmdConfigLists, opts.Logger) | ||||||
| 	opts.Logger.Info().Str("using lists config file", opts.CmdListFile).Send() | 	opts.Logger.Info().Str("using lists config file", opts.CmdListFile).Send() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func generateFileFetchErrorString(file, fileType string, err error) string { | ||||||
|  | 	return fmt.Sprintf("Could not fetch %s file %s: %v", file, fileType, err) | ||||||
|  | } | ||||||
|  |  | ||||||
| func validateCommandLists(opts *ConfigOpts) { | func validateCommandLists(opts *ConfigOpts) { | ||||||
| 	var cmdNotFoundSliceErr []error | 	var cmdNotFoundSliceErr []error | ||||||
| 	for cmdListName, cmdList := range opts.CmdConfigLists { | 	for cmdListName, cmdList := range opts.CmdConfigLists { | ||||||
| @@ -427,7 +471,7 @@ func getLoggingKeyFromConfig(key string) string { | |||||||
| // 	return fmt.Sprintf("cmdLists.%s", list) | // 	return fmt.Sprintf("cmdLists.%s", list) | ||||||
| // } | // } | ||||||
|  |  | ||||||
| func (opts *ConfigOpts) setupVault() error { | func (opts *ConfigOpts) initializeVault() error { | ||||||
| 	if !opts.koanf.Bool("vault.enabled") { | 	if !opts.koanf.Bool("vault.enabled") { | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
| @@ -448,97 +492,41 @@ func (opts *ConfigOpts) setupVault() error { | |||||||
| 		token = os.Getenv("VAULT_TOKEN") | 		token = os.Getenv("VAULT_TOKEN") | ||||||
| 	} | 	} | ||||||
| 	if strings.TrimSpace(token) == "" { | 	if strings.TrimSpace(token) == "" { | ||||||
| 		return fmt.Errorf("no token found, but one was required. \n\nSet the config key vault.token or the environment variable VAULT_TOKEN") | 		return fmt.Errorf("no token found. One is required. \n\nSet the config key vault.token or the environment variable VAULT_TOKEN") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	client.SetToken(token) | 	client.SetToken(token) | ||||||
|  |  | ||||||
| 	unmarshalErr := opts.koanf.UnmarshalWithConf("vault.keys", &opts.VaultKeys, koanf.UnmarshalConf{Tag: "yaml"}) | 	unmarshalErr := opts.koanf.UnmarshalWithConf("vault.keys", &opts.VaultKeys, koanf.UnmarshalConf{Tag: "yaml"}) | ||||||
| 	if unmarshalErr != nil { | 	if unmarshalErr != nil { | ||||||
| 		logging.ExitWithMSG(fmt.Sprintf("error unmarshalling vault.keys into struct: %v", unmarshalErr), 1, &opts.Logger) | 		logging.ExitWithMSG(fmt.Sprintf("error unmarshaling vault.keys into struct: %v", unmarshalErr), 1, &opts.Logger) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	opts.vaultClient = client | 	opts.vaultClient = client | ||||||
|  |  | ||||||
|  | 	for _, v := range opts.VaultKeys { | ||||||
|  | 		v.Name = replaceVarInString(opts.Vars, v.Key, opts.Logger) | ||||||
|  | 		v.MountPath = replaceVarInString(opts.Vars, v.MountPath, opts.Logger) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func getVaultSecret(vaultClient *vault.Client, key *VaultKey) (string, error) { |  | ||||||
| 	var ( |  | ||||||
| 		secret *vault.KVSecret |  | ||||||
| 		err    error |  | ||||||
| 	) |  | ||||||
|  |  | ||||||
| 	if key.ValueType == "KVv2" { |  | ||||||
| 		secret, err = vaultClient.KVv2(key.MountPath).Get(context.Background(), key.Path) |  | ||||||
| 	} else if key.ValueType == "KVv1" { |  | ||||||
| 		secret, err = vaultClient.KVv1(key.MountPath).Get(context.Background(), key.Path) |  | ||||||
| 	} else if key.ValueType != "" { |  | ||||||
| 		return "", fmt.Errorf("type %s for key %s not known. Valid types are KVv1 or KVv2", key.ValueType, key.Name) |  | ||||||
| 	} else { |  | ||||||
| 		return "", fmt.Errorf("type for key %s must be specified. Valid types are KVv1 or KVv2", key.Name) |  | ||||||
|  |  | ||||||
| 	} |  | ||||||
| 	if err != nil { |  | ||||||
| 		return "", fmt.Errorf("unable to read secret: %v", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	value, ok := secret.Data[key.Name].(string) |  | ||||||
| 	if !ok { |  | ||||||
| 		return "", fmt.Errorf("value type assertion failed: %T %#v", secret.Data[key.Name], secret.Data[key.Name]) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return value, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func isVaultKey(str string) (string, bool) { |  | ||||||
| 	str = strings.TrimSpace(str) |  | ||||||
| 	return strings.TrimPrefix(str, "vault:"), strings.HasPrefix(str, "vault:") |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func parseVaultKey(str string, keys []*VaultKey) (*VaultKey, error) { |  | ||||||
| 	keyName, isKey := isVaultKey(str) |  | ||||||
| 	if !isKey { |  | ||||||
| 		return nil, nil |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	for _, k := range keys { |  | ||||||
| 		if k.Name == keyName { |  | ||||||
| 			return k, nil |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return nil, fmt.Errorf("key %s not found in vault keys", keyName) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func GetVaultKey(str string, opts *ConfigOpts, log zerolog.Logger) string { |  | ||||||
| 	key, err := parseVaultKey(str, opts.VaultKeys) |  | ||||||
| 	if key == nil && err == nil { |  | ||||||
| 		return str |  | ||||||
| 	} |  | ||||||
| 	if err != nil && key == nil { |  | ||||||
| 		log.Err(err).Send() |  | ||||||
| 		return "" |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	value, secretErr := getVaultSecret(opts.vaultClient, key) |  | ||||||
| 	if secretErr != nil { |  | ||||||
| 		log.Err(secretErr).Send() |  | ||||||
| 		return value |  | ||||||
| 	} |  | ||||||
| 	return value |  | ||||||
| } |  | ||||||
|  |  | ||||||
| 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 { | ||||||
|  | 		cmd.GetVariablesFromConf(opts) | ||||||
|  | 		cmd.Cmd = replaceVarInString(opts.Vars, cmd.Cmd, opts.Logger) | ||||||
|  | 		for i, v := range cmd.Args { | ||||||
|  | 			v = replaceVarInString(opts.Vars, v, opts.Logger) | ||||||
|  | 			cmd.Args[i] = v | ||||||
|  | 		} | ||||||
| 		if cmd.Name == "" { | 		if cmd.Name == "" { | ||||||
| 			cmd.Name = cmdName | 			cmd.Name = cmdName | ||||||
| 		} | 		} | ||||||
| 		// println("Cmd.Name = " + cmd.Name) |  | ||||||
| 		hooks := cmd.Hooks | 		hooks := cmd.Hooks | ||||||
| 		// resolve hooks |  | ||||||
| 		if hooks != nil { | 		if hooks != nil { | ||||||
|  |  | ||||||
| 			processHookSuccess := processHooks(cmd, hooks.Error, opts, "error") | 			processHookSuccess := processHooks(cmd, hooks.Error, opts, "error") | ||||||
| @@ -555,9 +543,13 @@ func processCmds(opts *ConfigOpts) error { | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// resolve hosts | 		if !IsHostLocal(cmd.Host) { | ||||||
| 		if cmd.Host != nil { |  | ||||||
| 			host, hostFound := opts.Hosts[*cmd.Host] | 			cmdHost := replaceVarInString(opts.Vars, cmd.Host, opts.Logger) | ||||||
|  | 			if cmdHost != cmd.Host { | ||||||
|  | 				cmd.Host = cmdHost | ||||||
|  | 			} | ||||||
|  | 			host, hostFound := opts.Hosts[cmd.Host] | ||||||
| 			if hostFound { | 			if hostFound { | ||||||
| 				cmd.RemoteHost = host | 				cmd.RemoteHost = host | ||||||
| 				cmd.RemoteHost.Host = host.Host | 				cmd.RemoteHost.Host = host.Host | ||||||
| @@ -565,8 +557,12 @@ func processCmds(opts *ConfigOpts) error { | |||||||
| 					cmd.RemoteHost.HostName = host.HostName | 					cmd.RemoteHost.HostName = host.HostName | ||||||
| 				} | 				} | ||||||
| 			} else { | 			} else { | ||||||
| 				opts.Hosts[*cmd.Host] = &Host{Host: *cmd.Host} | 				opts.Logger.Info().Msgf("adding host %s to host list", cmd.Host) | ||||||
| 				cmd.RemoteHost = &Host{Host: *cmd.Host} | 				if opts.Hosts == nil { | ||||||
|  | 					opts.Hosts = make(map[string]*Host) | ||||||
|  | 				} | ||||||
|  | 				opts.Hosts[cmd.Host] = &Host{Host: cmd.Host} | ||||||
|  | 				cmd.RemoteHost = &Host{Host: cmd.Host} | ||||||
| 			} | 			} | ||||||
| 		} else { | 		} else { | ||||||
|  |  | ||||||
| @@ -577,19 +573,20 @@ func processCmds(opts *ConfigOpts) error { | |||||||
| 					return err | 					return err | ||||||
| 				} | 				} | ||||||
| 				cmd.Dir = &cmdDir | 				cmd.Dir = &cmdDir | ||||||
|  | 			} else { | ||||||
|  | 				cmd.Dir = &opts.ConfigDir | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Parse package commands | 		if cmd.Type == PackageCommandType { | ||||||
| 		if cmd.Type == PackageCT { |  | ||||||
| 			if cmd.PackageManager == "" { | 			if cmd.PackageManager == "" { | ||||||
| 				return fmt.Errorf("package manager is required for package command %s", cmd.PackageName) | 				return fmt.Errorf("package manager is required for package command %s", cmd.Name) | ||||||
| 			} | 			} | ||||||
| 			if cmd.PackageOperation.String() == "" { | 			if cmd.PackageOperation.String() == "" { | ||||||
| 				return fmt.Errorf("package operation is required for package command %s", cmd.PackageName) | 				return fmt.Errorf("package operation is required for package command %s", cmd.Name) | ||||||
| 			} | 			} | ||||||
| 			if cmd.PackageName == "" { | 			if cmd.Packages == nil { | ||||||
| 				return fmt.Errorf("package name is required for package command %s", cmd.PackageName) | 				return fmt.Errorf("package name is required for package command %s", cmd.Name) | ||||||
| 			} | 			} | ||||||
| 			var err error | 			var err error | ||||||
|  |  | ||||||
| @@ -607,24 +604,38 @@ func processCmds(opts *ConfigOpts) error { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Parse user commands | 		// Parse user commands | ||||||
| 		if cmd.Type == UserCT { | 		if cmd.Type == UserCommandType { | ||||||
| 			if cmd.Username == "" { | 			if cmd.Username == "" { | ||||||
| 				return fmt.Errorf("username is required for user command %s", cmd.Name) | 				return fmt.Errorf("username is required for user command %s", cmd.Name) | ||||||
| 			} | 			} | ||||||
|  | 			cmd.Username = replaceVarInString(opts.Vars, cmd.Username, opts.Logger) | ||||||
| 			detectOSType(cmd, opts) | 			err := detectOSType(cmd, opts) | ||||||
| 			var err error | 			if err != nil { | ||||||
|  | 				opts.Logger.Info().Err(err).Str("command", cmdName).Send() | ||||||
|  | 			} | ||||||
|  |  | ||||||
| 			// Validate the operation | 			// Validate the operation | ||||||
| 			switch cmd.UserOperation { | 			switch cmd.UserOperation { | ||||||
| 			case "add", "remove", "modify", "checkIfExists", "delete", "password": | 			case "add", "remove", "modify", "checkIfExists", "delete", "password": | ||||||
| 				cmd.userMan, err = usermanager.NewUserManager(cmd.OS) | 				cmd.userMan, err = usermanager.NewUserManager(cmd.OS) | ||||||
| 				if cmd.Host != nil { |  | ||||||
| 					host, ok := opts.Hosts[*cmd.Host] | 				if cmd.UserOperation == "password" { | ||||||
|  | 					opts.Logger.Debug().Msg("changing password for user: " + cmd.Username) | ||||||
|  | 					cmd.UserPassword = getExternalConfigDirectiveValue(cmd.UserPassword, opts, AllowedExternalDirectiveAll) | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				if !IsHostLocal(cmd.Host) { | ||||||
|  |  | ||||||
|  | 					host, ok := opts.Hosts[cmd.Host] | ||||||
| 					if ok { | 					if ok { | ||||||
| 						cmd.userMan, err = usermanager.NewUserManager(host.OS) | 						cmd.userMan, err = usermanager.NewUserManager(host.OS) | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
|  | 				for indx, key := range cmd.UserSshPubKeys { | ||||||
|  | 					opts.Logger.Debug().Msg("adding SSH Keys") | ||||||
|  | 					key = getExternalConfigDirectiveValue(key, opts, AllowedExternalDirectiveAll) | ||||||
|  | 					cmd.UserSshPubKeys[indx] = key | ||||||
|  | 				} | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
| 					return err | 					return err | ||||||
| 				} | 				} | ||||||
| @@ -634,7 +645,7 @@ func processCmds(opts *ConfigOpts) error { | |||||||
|  |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if cmd.Type == RemoteScriptCT { | 		if cmd.Type == RemoteScriptCommandType { | ||||||
| 			var fetchErr error | 			var fetchErr error | ||||||
| 			if !isRemoteURL(cmd.Cmd) { | 			if !isRemoteURL(cmd.Cmd) { | ||||||
| 				return fmt.Errorf("remoteScript command %s must be a remote resource", cmdName) | 				return fmt.Errorf("remoteScript command %s must be a remote resource", cmdName) | ||||||
| @@ -645,9 +656,9 @@ func processCmds(opts *ConfigOpts) error { | |||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 		} | 		} | ||||||
| 		if cmd.OutputFile != "" { | 		if cmd.Output.File != "" { | ||||||
| 			var err error | 			var err error | ||||||
| 			cmd.OutputFile, err = getFullPathWithHomeDir(cmd.OutputFile) | 			cmd.Output.File, err = getFullPathWithHomeDir(cmd.Output.File) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return err | 				return err | ||||||
| 			} | 			} | ||||||
| @@ -656,14 +667,8 @@ func processCmds(opts *ConfigOpts) error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // processHooks evaluates if hooks are valid Commands |  | ||||||
| // |  | ||||||
| // The cmd.hookRefs[hookType] is created with any hooks found. |  | ||||||
| // |  | ||||||
| // Returns an error, if any, if the hook command is not found |  | ||||||
| func processHooks(cmd *Command, hooks []string, opts *ConfigOpts, hookType string) error { | func processHooks(cmd *Command, hooks []string, opts *ConfigOpts, hookType string) error { | ||||||
|  |  | ||||||
| 	// initialize hook type |  | ||||||
| 	var hookCmdFound bool | 	var hookCmdFound bool | ||||||
| 	cmd.hookRefs = map[string]map[string]*Command{} | 	cmd.hookRefs = map[string]map[string]*Command{} | ||||||
| 	cmd.hookRefs[hookType] = map[string]*Command{} | 	cmd.hookRefs[hookType] = map[string]*Command{} | ||||||
| @@ -690,13 +695,18 @@ func processHooks(cmd *Command, hooks []string, opts *ConfigOpts, hookType strin | |||||||
| } | } | ||||||
|  |  | ||||||
| func detectOSType(cmd *Command, opts *ConfigOpts) error { | func detectOSType(cmd *Command, opts *ConfigOpts) error { | ||||||
| 	if cmd.Host == nil { |  | ||||||
| 		if runtime.GOOS == "linux" { // also can be specified to FreeBSD | 	if IsHostLocal(cmd.Host) { | ||||||
|  |  | ||||||
|  | 		if runtime.GOOS == "linux" { | ||||||
| 			cmd.OS = "linux" | 			cmd.OS = "linux" | ||||||
| 			opts.Logger.Info().Msg("Unix/Linux type OS detected") | 			opts.Logger.Info().Msg("Unix/Linux type OS detected") | ||||||
|  | 			return nil | ||||||
| 		} | 		} | ||||||
|  | 		return fmt.Errorf("using an os that is not yet supported for user commands") | ||||||
| 	} | 	} | ||||||
| 	host, ok := opts.Hosts[*cmd.Host] |  | ||||||
|  | 	host, ok := opts.Hosts[cmd.Host] | ||||||
| 	if ok { | 	if ok { | ||||||
| 		if host.OS != "" { | 		if host.OS != "" { | ||||||
| 			return nil | 			return nil | ||||||
| @@ -728,3 +738,25 @@ func keyNotSupported(oldKey, newKey string, koanf *koanf.Koanf, opts *ConfigOpts | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func replaceVarInString(vars map[string]string, str string, logger zerolog.Logger) string { | ||||||
|  | 	if strings.Contains(str, "%{var:") && strings.Contains(str, "}%") { | ||||||
|  | 		logger.Debug().Msgf("replacing vars in string %s", str) | ||||||
|  | 		for k, v := range vars { | ||||||
|  | 			if strings.Contains(str, "%{var:"+k+"}%") { | ||||||
|  | 				str = strings.ReplaceAll(str, "%{var:"+k+"}%", v) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		if strings.Contains(str, "%{var:") && strings.Contains(str, "}%") { | ||||||
|  | 			logger.Warn().Msg("could not replace all vars in string") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return str | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Command) GetVariablesFromConf(opts *ConfigOpts) { | ||||||
|  | 	c.ScriptEnvFile = replaceVarInString(opts.Vars, c.ScriptEnvFile, opts.Logger) | ||||||
|  | 	c.Name = replaceVarInString(opts.Vars, c.Name, opts.Logger) | ||||||
|  | 	c.Output.File = replaceVarInString(opts.Vars, c.Output.File, opts.Logger) | ||||||
|  | 	c.Host = replaceVarInString(opts.Vars, c.Host, opts.Logger) | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										133
									
								
								pkg/backy/lineinfile.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								pkg/backy/lineinfile.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,133 @@ | |||||||
|  | package backy | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bufio" | ||||||
|  | 	"bytes" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"regexp" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"golang.org/x/crypto/ssh" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func sshConnect(user, password, host string, port int) (*ssh.Client, error) { | ||||||
|  | 	config := &ssh.ClientConfig{ | ||||||
|  | 		User: user, | ||||||
|  | 		Auth: []ssh.AuthMethod{ | ||||||
|  | 			ssh.Password(password), | ||||||
|  | 		}, | ||||||
|  | 		HostKeyCallback: ssh.InsecureIgnoreHostKey(), | ||||||
|  | 	} | ||||||
|  | 	addr := fmt.Sprintf("%s:%d", host, port) | ||||||
|  | 	return ssh.Dial("tcp", addr, config) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func sshReadFile(client *ssh.Client, remotePath string) (string, error) { | ||||||
|  | 	session, err := client.NewSession() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	defer session.Close() | ||||||
|  |  | ||||||
|  | 	var b bytes.Buffer | ||||||
|  | 	session.Stdout = &b | ||||||
|  | 	if err := session.Run(fmt.Sprintf("cat %s", remotePath)); err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	return b.String(), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func sshWriteFile(client *ssh.Client, remotePath, content string) error { | ||||||
|  | 	session, err := client.NewSession() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	defer session.Close() | ||||||
|  |  | ||||||
|  | 	stdin, err := session.StdinPipe() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	go func() { | ||||||
|  | 		defer stdin.Close() | ||||||
|  | 		io.WriteString(stdin, content) | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	cmd := fmt.Sprintf("cat > %s", remotePath) | ||||||
|  | 	return session.Run(cmd) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func lineInString(content, regexpPattern, line string) string { | ||||||
|  | 	scanner := bufio.NewScanner(strings.NewReader(content)) | ||||||
|  | 	var lines []string | ||||||
|  | 	found := false | ||||||
|  | 	re := regexp.MustCompile(regexpPattern) | ||||||
|  |  | ||||||
|  | 	for scanner.Scan() { | ||||||
|  | 		l := scanner.Text() | ||||||
|  | 		if re.MatchString(l) { | ||||||
|  | 			found = true | ||||||
|  | 			lines = append(lines, line) | ||||||
|  | 		} else { | ||||||
|  | 			lines = append(lines, l) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if !found { | ||||||
|  | 		lines = append(lines, line) | ||||||
|  | 	} | ||||||
|  | 	return strings.Join(lines, "\n") + "\n" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func Call() { | ||||||
|  | 	user := "youruser" | ||||||
|  | 	password := "yourpassword" | ||||||
|  | 	host := "yourhost" | ||||||
|  | 	port := 22 | ||||||
|  | 	remotePath := "/path/to/remote/file" | ||||||
|  |  | ||||||
|  | 	client, err := sshConnect(user, password, host, port) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Println("SSH connection error:", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	defer client.Close() | ||||||
|  |  | ||||||
|  | 	content, err := sshReadFile(client, remotePath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Println("Read error:", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	newContent := lineInString(content, "^foo=", "foo=bar") | ||||||
|  |  | ||||||
|  | 	if err := sshWriteFile(client, remotePath, newContent); err != nil { | ||||||
|  | 		fmt.Println("Write error:", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	fmt.Println("Line updated successfully over SSH.") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type LineInFile struct { | ||||||
|  | 	RemotePath    string         // Path to the remote file | ||||||
|  | 	Pattern       string         // Regex pattern to match lines | ||||||
|  | 	Line          string         // Line to insert or replace | ||||||
|  | 	InsertAfter   bool           // If true, insert after matched line; else replace | ||||||
|  | 	User          string         // SSH username | ||||||
|  | 	Password      string         // SSH password (use key for production) | ||||||
|  | 	Host          string         // SSH host | ||||||
|  | 	Port          int            // SSH port | ||||||
|  | 	regexCompiled *regexp.Regexp // Compiled regex (internal use) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // CompileRegex compiles the regex pattern for later use | ||||||
|  | func (l *LineInFile) CompileRegex() error { | ||||||
|  | 	re, err := regexp.Compile(l.Pattern) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	l.regexCompiled = re | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
| @@ -46,9 +46,9 @@ func (opts *ConfigOpts) ListCommand(cmd string) { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// is it remote or local | 		// is it remote or local | ||||||
| 		if cmdInfo.Host != nil { | 		if !IsHostLocal(cmdInfo.Host) { | ||||||
| 			println() | 			println() | ||||||
| 			print("Host: ", *cmdInfo.Host) | 			print("Host: ", cmdInfo.Host) | ||||||
| 			println() | 			println() | ||||||
|  |  | ||||||
| 		} else { | 		} else { | ||||||
|   | |||||||
							
								
								
									
										88
									
								
								pkg/backy/metrics.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								pkg/backy/metrics.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | |||||||
|  | package backy | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"os" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type MetricFile struct { | ||||||
|  | 	Filename       string              `json:"filename"` | ||||||
|  | 	CommandMetrics map[string]*Metrics `json:"commandMetrics"` | ||||||
|  | 	ListMetrics    map[string]*Metrics `json:"listMetrics"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Metrics struct { | ||||||
|  | 	DateStartedLast              string  `json:"dateStartedLast"` | ||||||
|  | 	DateLastFinished             string  `json:"dateLastFinished"` | ||||||
|  | 	DateLastFinishedSuccessfully string  `json:"dateLastFinishedSuccessfully"` | ||||||
|  | 	SuccessfulExecutions         uint64  `json:"successfulExecutions"` | ||||||
|  | 	FailedExecutions             uint64  `json:"failedExecutions"` | ||||||
|  | 	TotalExecutions              uint64  `json:"totalExecutions"` | ||||||
|  | 	TotalExecutionTime           float64 `json:"lastExecutionTime"`  // in seconds | ||||||
|  | 	AverageExecutionTime         float64 `json:"totalExecutionTime"` // in seconds | ||||||
|  | 	SuccessRate                  float64 `json:"successRate"`        // percentage of successful executions | ||||||
|  | 	FailureRate                  float64 `json:"failureRate"`        // percentage of failed executions | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func NewMetrics() *Metrics { | ||||||
|  | 	return &Metrics{ | ||||||
|  | 		DateStartedLast:      time.Now().Format(time.RFC3339), | ||||||
|  | 		SuccessfulExecutions: 0, | ||||||
|  | 		FailedExecutions:     0, | ||||||
|  | 		TotalExecutions:      0, | ||||||
|  | 		TotalExecutionTime:   0.0, | ||||||
|  | 		AverageExecutionTime: 0.0, | ||||||
|  | 		SuccessRate:          0.0, | ||||||
|  | 		FailureRate:          0.0, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func NewMetricsFromFile(filename string) *MetricFile { | ||||||
|  | 	return &MetricFile{ | ||||||
|  | 		Filename:       filename, | ||||||
|  | 		CommandMetrics: make(map[string]*Metrics), | ||||||
|  | 		ListMetrics:    make(map[string]*Metrics), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *Metrics) Update(success bool, executionTime float64, dateLastFinished time.Time) { | ||||||
|  | 	m.TotalExecutions++ | ||||||
|  | 	if success { | ||||||
|  | 		m.SuccessfulExecutions++ | ||||||
|  | 	} else { | ||||||
|  | 		m.FailedExecutions++ | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	m.DateLastFinished = dateLastFinished.Format(time.RFC3339) | ||||||
|  |  | ||||||
|  | 	m.TotalExecutionTime += executionTime | ||||||
|  | 	m.AverageExecutionTime = m.TotalExecutionTime / float64(m.TotalExecutions) | ||||||
|  |  | ||||||
|  | 	if m.TotalExecutions > 0 { | ||||||
|  | 		m.SuccessRate = float64(m.SuccessfulExecutions) / float64(m.TotalExecutions) * 100 | ||||||
|  | 		m.FailureRate = float64(m.FailedExecutions) / float64(m.TotalExecutions) * 100 | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (metricFile *MetricFile) SaveToFile() error { | ||||||
|  | 	data, err := json.MarshalIndent(metricFile, "", "  ") | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return os.WriteFile(metricFile.Filename, data, 0644) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func LoadMetricsFromFile(filename string) (*MetricFile, error) { | ||||||
|  | 	jsonData, err := os.ReadFile(filename) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	var metrics MetricFile | ||||||
|  | 	err = json.Unmarshal(jsonData, &metrics) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return &metrics, nil | ||||||
|  | } | ||||||
							
								
								
									
										67
									
								
								pkg/backy/metrics_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								pkg/backy/metrics_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | |||||||
|  | package backy | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestAddingMetricsForCommand(t *testing.T) { | ||||||
|  |  | ||||||
|  | 	// Create a new MetricFile | ||||||
|  | 	metricFile := NewMetricsFromFile("test_metrics.json") | ||||||
|  |  | ||||||
|  | 	metricFile, err := LoadMetricsFromFile(metricFile.Filename) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Errorf("Failed to load metrics from file: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Add metrics for a command | ||||||
|  | 	commandName := "test_command" | ||||||
|  | 	if _, exists := metricFile.CommandMetrics[commandName]; !exists { | ||||||
|  | 		metricFile.CommandMetrics[commandName] = NewMetrics() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Update the metrics for the command | ||||||
|  | 	executionTime := 1.8 // Example execution time in seconds | ||||||
|  | 	success := true      // Example success status | ||||||
|  | 	metricFile.CommandMetrics[commandName].Update(success, executionTime, time.Now()) | ||||||
|  |  | ||||||
|  | 	// Check if the metrics were updated correctly | ||||||
|  | 	if metricFile.CommandMetrics[commandName].SuccessfulExecutions > 50 { | ||||||
|  | 		t.Errorf("Expected 1 successful execution, got %d", metricFile.CommandMetrics[commandName].SuccessfulExecutions) | ||||||
|  | 	} | ||||||
|  | 	if metricFile.CommandMetrics[commandName].TotalExecutions > 50 { | ||||||
|  | 		t.Errorf("Expected 1 total execution, got %d", metricFile.CommandMetrics[commandName].TotalExecutions) | ||||||
|  | 	} | ||||||
|  | 	// if metricFile.CommandMetrics[commandName].TotalExecutionTime != executionTime { | ||||||
|  | 	// 	t.Errorf("Expected execution time %f, got %f", executionTime, metricFile.CommandMetrics[commandName].TotalExecutionTime) | ||||||
|  | 	// } | ||||||
|  |  | ||||||
|  | 	err = metricFile.SaveToFile() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Errorf("Failed to save metrics to file: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	listName := "test_list" | ||||||
|  | 	if _, exists := metricFile.ListMetrics[listName]; !exists { | ||||||
|  | 		metricFile.ListMetrics[listName] = NewMetrics() | ||||||
|  | 	} | ||||||
|  | 	// Update the metrics for the list | ||||||
|  | 	metricFile.ListMetrics[listName].Update(success, executionTime, time.Now()) | ||||||
|  | 	if metricFile.ListMetrics[listName].SuccessfulExecutions > 50 { | ||||||
|  | 		t.Errorf("Expected 1 successful execution for list, got %d", metricFile.ListMetrics[listName].SuccessfulExecutions) | ||||||
|  | 	} | ||||||
|  | 	if metricFile.ListMetrics[listName].TotalExecutions > 50 { | ||||||
|  | 		t.Errorf("Expected 1 total execution for list, got %d", metricFile.ListMetrics[listName].TotalExecutions) | ||||||
|  | 	} | ||||||
|  | 	// if metricFile.ListMetrics[listName].TotalExecutionTime > executionTime { | ||||||
|  | 	// 	t.Errorf("Expected execution time %f for list, got %f", executionTime, metricFile.ListMetrics[listName].TotalExecutionTime) | ||||||
|  | 	// } | ||||||
|  |  | ||||||
|  | 	// Save the metrics to a file | ||||||
|  | 	err = metricFile.SaveToFile() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Errorf("Failed to save metrics to file: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -9,6 +9,7 @@ import ( | |||||||
|  |  | ||||||
| 	"git.andrewnw.xyz/CyberShell/backy/pkg/logging" | 	"git.andrewnw.xyz/CyberShell/backy/pkg/logging" | ||||||
| 	"github.com/nikoksr/notify" | 	"github.com/nikoksr/notify" | ||||||
|  | 	"github.com/nikoksr/notify/service/http" | ||||||
| 	"github.com/nikoksr/notify/service/mail" | 	"github.com/nikoksr/notify/service/mail" | ||||||
| 	"github.com/nikoksr/notify/service/matrix" | 	"github.com/nikoksr/notify/service/matrix" | ||||||
| 	"maunium.net/go/mautrix/id" | 	"maunium.net/go/mautrix/id" | ||||||
| @@ -30,6 +31,12 @@ type MailConfig struct { | |||||||
| 	Password      string   `yaml:"password"` | 	Password      string   `yaml:"password"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type HttpConfig struct { | ||||||
|  | 	URL     string              `yaml:"url"` | ||||||
|  | 	Method  string              `yaml:"method"` | ||||||
|  | 	Headers map[string][]string `yaml:"headers"` | ||||||
|  | } | ||||||
|  |  | ||||||
| // SetupNotify sets up notify instances for each command list. | // SetupNotify sets up notify instances for each command list. | ||||||
| func (opts *ConfigOpts) SetupNotify() { | func (opts *ConfigOpts) SetupNotify() { | ||||||
|  |  | ||||||
| @@ -58,6 +65,8 @@ func (opts *ConfigOpts) SetupNotify() { | |||||||
| 					opts.Logger.Info().Err(fmt.Errorf("error: ID %s not found in mail object", confId)).Str("list", confName).Send() | 					opts.Logger.Info().Err(fmt.Errorf("error: ID %s not found in mail object", confId)).Str("list", confName).Send() | ||||||
| 					continue | 					continue | ||||||
| 				} | 				} | ||||||
|  | 				conf.Password = getExternalConfigDirectiveValue(conf.Password, opts, AllowedExternalDirectiveAll) | ||||||
|  | 				opts.Logger.Debug().Str("list", confName).Str("id", confId).Msg("adding mail notification service") | ||||||
| 				mailConf := setupMail(conf) | 				mailConf := setupMail(conf) | ||||||
| 				services = append(services, mailConf) | 				services = append(services, mailConf) | ||||||
| 			case "matrix": | 			case "matrix": | ||||||
| @@ -66,14 +75,23 @@ func (opts *ConfigOpts) SetupNotify() { | |||||||
| 					opts.Logger.Info().Err(fmt.Errorf("error: ID %s not found in matrix object", confId)).Str("list", confName).Send() | 					opts.Logger.Info().Err(fmt.Errorf("error: ID %s not found in matrix object", confId)).Str("list", confName).Send() | ||||||
| 					continue | 					continue | ||||||
| 				} | 				} | ||||||
|  | 				conf.AccessToken = getExternalConfigDirectiveValue(conf.AccessToken, opts, AllowedExternalDirectiveAll) | ||||||
|  | 				opts.Logger.Debug().Str("list", confName).Str("id", confId).Msg("adding matrix notification service") | ||||||
| 				mtrxConf, mtrxErr := setupMatrix(conf) | 				mtrxConf, mtrxErr := setupMatrix(conf) | ||||||
| 				if mtrxErr != nil { | 				if mtrxErr != nil { | ||||||
| 					opts.Logger.Info().Str("list", confName).Err(fmt.Errorf("error: configuring matrix id %s failed during setup: %w", id, mtrxErr)) | 					opts.Logger.Info().Str("list", confName).Err(fmt.Errorf("error: configuring matrix id %s failed during setup: %w", id, mtrxErr)) | ||||||
| 					continue | 					continue | ||||||
| 				} | 				} | ||||||
| 				// append the services |  | ||||||
| 				services = append(services, mtrxConf) | 				services = append(services, mtrxConf) | ||||||
| 			// service is not recognized | 			case "http": | ||||||
|  | 				conf, ok := opts.NotificationConf.HttpConfig[confId] | ||||||
|  | 				if !ok { | ||||||
|  | 					opts.Logger.Info().Err(fmt.Errorf("error: ID %s not found in http object", confId)).Str("list", confName).Send() | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 				opts.Logger.Debug().Str("list", confName).Str("id", confId).Msg("adding http notification service") | ||||||
|  | 				httpConf := setupHttp(conf) | ||||||
|  | 				services = append(services, httpConf) | ||||||
| 			default: | 			default: | ||||||
| 				opts.Logger.Info().Err(fmt.Errorf("id %s not found", id)).Str("list", confName).Send() | 				opts.Logger.Info().Err(fmt.Errorf("id %s not found", id)).Str("list", confName).Send() | ||||||
| 			} | 			} | ||||||
| @@ -99,3 +117,19 @@ func setupMail(config MailConfig) *mail.Mail { | |||||||
| 	mailClient.BodyFormat(mail.PlainText) | 	mailClient.BodyFormat(mail.PlainText) | ||||||
| 	return mailClient | 	return mailClient | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func setupHttp(httpConf HttpConfig) *http.Service { | ||||||
|  |  | ||||||
|  | 	httpService := http.New() | ||||||
|  | 	httpService.AddReceivers(&http.Webhook{ | ||||||
|  | 		URL:         httpConf.URL, | ||||||
|  | 		Header:      httpConf.Headers, | ||||||
|  | 		ContentType: "text/plain", | ||||||
|  | 		Method:      httpConf.Method, | ||||||
|  | 		BuildPayload: func(subject, message string) (payload any) { | ||||||
|  | 			return subject + "\n\n" + message | ||||||
|  | 		}, | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	return httpService | ||||||
|  | } | ||||||
|   | |||||||
| @@ -26,31 +26,31 @@ func (i PackageOperation) String() string { | |||||||
| func _PackageOperationNoOp() { | func _PackageOperationNoOp() { | ||||||
| 	var x [1]struct{} | 	var x [1]struct{} | ||||||
| 	_ = x[DefaultPO-(0)] | 	_ = x[DefaultPO-(0)] | ||||||
| 	_ = x[PackOpInstall-(1)] | 	_ = x[PackageOperationInstall-(1)] | ||||||
| 	_ = x[PackOpUpgrade-(2)] | 	_ = x[PackageOperationUpgrade-(2)] | ||||||
| 	_ = x[PackOpPurge-(3)] | 	_ = x[PackageOperationPurge-(3)] | ||||||
| 	_ = x[PackOpRemove-(4)] | 	_ = x[PackageOperationRemove-(4)] | ||||||
| 	_ = x[PackOpCheckVersion-(5)] | 	_ = x[PackageOperationCheckVersion-(5)] | ||||||
| 	_ = x[PackOpIsInstalled-(6)] | 	_ = x[PackageOperationIsInstalled-(6)] | ||||||
| } | } | ||||||
|  |  | ||||||
| var _PackageOperationValues = []PackageOperation{DefaultPO, PackOpInstall, PackOpUpgrade, PackOpPurge, PackOpRemove, PackOpCheckVersion, PackOpIsInstalled} | var _PackageOperationValues = []PackageOperation{DefaultPO, PackageOperationInstall, PackageOperationUpgrade, PackageOperationPurge, PackageOperationRemove, PackageOperationCheckVersion, PackageOperationIsInstalled} | ||||||
|  |  | ||||||
| var _PackageOperationNameToValueMap = map[string]PackageOperation{ | var _PackageOperationNameToValueMap = map[string]PackageOperation{ | ||||||
| 	_PackageOperationName[0:0]:        DefaultPO, | 	_PackageOperationName[0:0]:        DefaultPO, | ||||||
| 	_PackageOperationLowerName[0:0]:   DefaultPO, | 	_PackageOperationLowerName[0:0]:   DefaultPO, | ||||||
| 	_PackageOperationName[0:7]:        PackOpInstall, | 	_PackageOperationName[0:7]:        PackageOperationInstall, | ||||||
| 	_PackageOperationLowerName[0:7]:   PackOpInstall, | 	_PackageOperationLowerName[0:7]:   PackageOperationInstall, | ||||||
| 	_PackageOperationName[7:14]:       PackOpUpgrade, | 	_PackageOperationName[7:14]:       PackageOperationUpgrade, | ||||||
| 	_PackageOperationLowerName[7:14]:  PackOpUpgrade, | 	_PackageOperationLowerName[7:14]:  PackageOperationUpgrade, | ||||||
| 	_PackageOperationName[14:19]:      PackOpPurge, | 	_PackageOperationName[14:19]:      PackageOperationPurge, | ||||||
| 	_PackageOperationLowerName[14:19]: PackOpPurge, | 	_PackageOperationLowerName[14:19]: PackageOperationPurge, | ||||||
| 	_PackageOperationName[19:25]:      PackOpRemove, | 	_PackageOperationName[19:25]:      PackageOperationRemove, | ||||||
| 	_PackageOperationLowerName[19:25]: PackOpRemove, | 	_PackageOperationLowerName[19:25]: PackageOperationRemove, | ||||||
| 	_PackageOperationName[25:37]:      PackOpCheckVersion, | 	_PackageOperationName[25:37]:      PackageOperationCheckVersion, | ||||||
| 	_PackageOperationLowerName[25:37]: PackOpCheckVersion, | 	_PackageOperationLowerName[25:37]: PackageOperationCheckVersion, | ||||||
| 	_PackageOperationName[37:48]:      PackOpIsInstalled, | 	_PackageOperationName[37:48]:      PackageOperationIsInstalled, | ||||||
| 	_PackageOperationLowerName[37:48]: PackOpIsInstalled, | 	_PackageOperationLowerName[37:48]: PackageOperationIsInstalled, | ||||||
| } | } | ||||||
|  |  | ||||||
| var _PackageOperationNames = []string{ | var _PackageOperationNames = []string{ | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								pkg/backy/planForHosts.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								pkg/backy/planForHosts.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | # Running commands on hosts | ||||||
|  |  | ||||||
|  | I want all commands in a list to be able to be run on all hosts. The underlying solution will be using a function to run the list on a host, and therefore change the host on the commands. This can be done in several ways: | ||||||
|  |  | ||||||
|  | 1. The commands can have a `Hosts` field that will be a []string. This array can be populated several ways: | ||||||
|  |     - From the config file | ||||||
|  |     - using CLI options and commands | ||||||
|  |    The commands can be run in succession on all hosts using functions | ||||||
|  |  | ||||||
|  | 2. The existing `Host` field can be modified in a function. The commands need to be added to a `[]*Command` slice so that all hosts can be run. | ||||||
							
								
								
									
										415
									
								
								pkg/backy/ssh.go
									
									
									
									
									
								
							
							
						
						
									
										415
									
								
								pkg/backy/ssh.go
									
									
									
									
									
								
							| @@ -15,50 +15,56 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/google/uuid" | ||||||
| 	"github.com/kevinburke/ssh_config" | 	"github.com/kevinburke/ssh_config" | ||||||
| 	"github.com/pkg/errors" | 	"github.com/pkg/errors" | ||||||
|  | 	"github.com/pkg/sftp" | ||||||
| 	"github.com/rs/zerolog" | 	"github.com/rs/zerolog" | ||||||
| 	"golang.org/x/crypto/ssh" | 	"golang.org/x/crypto/ssh" | ||||||
| 	"golang.org/x/crypto/ssh/knownhosts" | 	"golang.org/x/crypto/ssh/knownhosts" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var PrivateKeyExtraInfoErr = errors.New("Private key may be encrypted. \nIf encrypted, make sure the password is specified correctly in the correct section. This may be done in one of three ways: \n privatekeypassword: env:PR_KEY_PASS \n privatekeypassword: file:/path/to/password-file \n privatekeypassword: password (not recommended). \n ") | var PrivateKeyExtraInfoErr = errors.New("Private key may be encrypted. \nIf encrypted, make sure the password is specified correctly in the correct section. This may be done in one of two ways: \n Using external directives - see docs \n privatekeypassword: password (not recommended). \n ") | ||||||
| var TS = strings.TrimSpace | var TS = strings.TrimSpace | ||||||
|  |  | ||||||
|  | type RemoteHostCommandExecutor interface { | ||||||
|  | 	RunCmdOnHost(command *Command, commandSession *ssh.Session, cmdCtxLogger zerolog.Logger, cmdOutBuf bytes.Buffer) ([]string, error) | ||||||
|  | } | ||||||
|  |  | ||||||
| // ConnectToHost connects to a host by looking up the config values in the file ~/.ssh/config | // ConnectToHost connects to a host by looking up the config values in the file ~/.ssh/config | ||||||
| // It uses any set values and looks up an unset values in the config files | // It uses any set values and looks up an unset values in the config files | ||||||
| // remoteConfig is modified directly. The *ssh.Client is returned as part of remoteConfig, | // remoteHost is modified directly. The *ssh.Client is returned as part of remoteHost, | ||||||
| // If configFile is empty, any required configuration is looked up in the default config files | // If configFile is empty, any required configuration is looked up in the default config files | ||||||
| // If any value is not found, defaults are used | // If any value is not found, defaults are used | ||||||
| func (remoteConfig *Host) ConnectToHost(opts *ConfigOpts) error { | func (remoteHost *Host) ConnectToHost(opts *ConfigOpts) error { | ||||||
|  |  | ||||||
| 	var connectErr error | 	var connectErr error | ||||||
|  |  | ||||||
| 	if TS(remoteConfig.ConfigFilePath) == "" { | 	if TS(remoteHost.ConfigFilePath) == "" { | ||||||
| 		remoteConfig.useDefaultConfig = true | 		remoteHost.useDefaultConfig = true | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	khPathErr := remoteConfig.GetKnownHosts() | 	khPathErr := remoteHost.GetKnownHosts() | ||||||
|  |  | ||||||
| 	if khPathErr != nil { | 	if khPathErr != nil { | ||||||
| 		return khPathErr | 		return khPathErr | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if remoteConfig.ClientConfig == nil { | 	if remoteHost.ClientConfig == nil { | ||||||
| 		remoteConfig.ClientConfig = &ssh.ClientConfig{} | 		remoteHost.ClientConfig = &ssh.ClientConfig{} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var configFile *os.File | 	var configFile *os.File | ||||||
|  |  | ||||||
| 	var sshConfigFileOpenErr error | 	var sshConfigFileOpenErr error | ||||||
|  |  | ||||||
| 	if !remoteConfig.useDefaultConfig { | 	if !remoteHost.useDefaultConfig { | ||||||
| 		var err error | 		var err error | ||||||
| 		remoteConfig.ConfigFilePath, err = getFullPathWithHomeDir(remoteConfig.ConfigFilePath) | 		remoteHost.ConfigFilePath, err = getFullPathWithHomeDir(remoteHost.ConfigFilePath) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		configFile, sshConfigFileOpenErr = os.Open(remoteConfig.ConfigFilePath) | 		configFile, sshConfigFileOpenErr = os.Open(remoteHost.ConfigFilePath) | ||||||
| 		if sshConfigFileOpenErr != nil { | 		if sshConfigFileOpenErr != nil { | ||||||
| 			return sshConfigFileOpenErr | 			return sshConfigFileOpenErr | ||||||
| 		} | 		} | ||||||
| @@ -69,22 +75,22 @@ func (remoteConfig *Host) ConnectToHost(opts *ConfigOpts) error { | |||||||
| 			return sshConfigFileOpenErr | 			return sshConfigFileOpenErr | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	remoteConfig.SSHConfigFile = &sshConfigFile{} | 	remoteHost.SSHConfigFile = &sshConfigFile{} | ||||||
| 	remoteConfig.SSHConfigFile.DefaultUserSettings = ssh_config.DefaultUserSettings | 	remoteHost.SSHConfigFile.DefaultUserSettings = ssh_config.DefaultUserSettings | ||||||
| 	var decodeErr error | 	var decodeErr error | ||||||
| 	remoteConfig.SSHConfigFile.SshConfigFile, decodeErr = ssh_config.Decode(configFile) | 	remoteHost.SSHConfigFile.SshConfigFile, decodeErr = ssh_config.Decode(configFile) | ||||||
| 	if decodeErr != nil { | 	if decodeErr != nil { | ||||||
| 		return decodeErr | 		return decodeErr | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	err := remoteConfig.GetProxyJumpFromConfig(opts.Hosts) | 	err := remoteHost.GetProxyJumpFromConfig(opts.Hosts) | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if remoteConfig.ProxyHost != nil { | 	if remoteHost.ProxyHost != nil { | ||||||
| 		for _, proxyHost := range remoteConfig.ProxyHost { | 		for _, proxyHost := range remoteHost.ProxyHost { | ||||||
| 			err := proxyHost.GetProxyJumpConfig(opts.Hosts, opts) | 			err := proxyHost.GetProxyJumpConfig(opts.Hosts, opts) | ||||||
| 			opts.Logger.Info().Msgf("Proxy host: %s", proxyHost.Host) | 			opts.Logger.Info().Msgf("Proxy host: %s", proxyHost.Host) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| @@ -93,50 +99,49 @@ func (remoteConfig *Host) ConnectToHost(opts *ConfigOpts) error { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	remoteConfig.ClientConfig.Timeout = time.Second * 30 | 	remoteHost.ClientConfig.Timeout = time.Second * 30 | ||||||
|  |  | ||||||
| 	remoteConfig.GetPrivateKeyFileFromConfig() | 	remoteHost.GetPrivateKeyFileFromConfig() | ||||||
|  |  | ||||||
| 	remoteConfig.GetPort() | 	remoteHost.GetPort() | ||||||
|  |  | ||||||
| 	remoteConfig.GetHostName() | 	remoteHost.GetHostName() | ||||||
|  |  | ||||||
| 	remoteConfig.CombineHostNameWithPort() | 	remoteHost.CombineHostNameWithPort() | ||||||
|  |  | ||||||
| 	remoteConfig.GetSshUserFromConfig() | 	remoteHost.GetSshUserFromConfig() | ||||||
|  |  | ||||||
| 	if remoteConfig.HostName == "" { | 	if remoteHost.HostName == "" { | ||||||
| 		return errors.Errorf("No hostname found or specified for host %s", remoteConfig.Host) | 		return errors.Errorf("No hostname found or specified for host %s", remoteHost.Host) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	err = remoteConfig.GetAuthMethods(opts) | 	err = remoteHost.GetAuthMethods(opts) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	hostKeyCallback, err := knownhosts.New(remoteConfig.KnownHostsFile) | 	hostKeyCallback, err := knownhosts.New(remoteHost.KnownHostsFile) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return errors.Wrap(err, "could not create hostkeycallback function") | 		return errors.Wrap(err, "could not create hostkeycallback function") | ||||||
| 	} | 	} | ||||||
| 	remoteConfig.ClientConfig.HostKeyCallback = hostKeyCallback | 	remoteHost.ClientConfig.HostKeyCallback = hostKeyCallback | ||||||
| 	// opts.Logger.Info().Str("user", remoteConfig.ClientConfig.User).Send() |  | ||||||
|  |  | ||||||
| 	remoteConfig.SshClient, connectErr = remoteConfig.ConnectThroughBastion(opts.Logger) | 	remoteHost.SshClient, connectErr = remoteHost.ConnectThroughBastion(opts.Logger) | ||||||
| 	if connectErr != nil { | 	if connectErr != nil { | ||||||
| 		return connectErr | 		return connectErr | ||||||
| 	} | 	} | ||||||
| 	if remoteConfig.SshClient != nil { | 	if remoteHost.SshClient != nil { | ||||||
| 		opts.Hosts[remoteConfig.Host] = remoteConfig | 		opts.Hosts[remoteHost.Host] = remoteHost | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	opts.Logger.Info().Msgf("Connecting to host %s", remoteConfig.HostName) | 	opts.Logger.Info().Msgf("Connecting to host %s", remoteHost.HostName) | ||||||
| 	remoteConfig.SshClient, connectErr = ssh.Dial("tcp", remoteConfig.HostName, remoteConfig.ClientConfig) | 	remoteHost.SshClient, connectErr = ssh.Dial("tcp", remoteHost.HostName, remoteHost.ClientConfig) | ||||||
| 	if connectErr != nil { | 	if connectErr != nil { | ||||||
| 		return connectErr | 		return connectErr | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	opts.Hosts[remoteConfig.Host] = remoteConfig | 	opts.Hosts[remoteHost.Host] = remoteHost | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -180,11 +185,7 @@ func (remoteHost *Host) GetAuthMethods(opts *ConfigOpts) error { | |||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		remoteHost.PrivateKeyPassword, err = GetPrivateKeyPassword(remoteHost.PrivateKeyPassword, opts, opts.Logger) | 		remoteHost.PrivateKeyPassword = GetPrivateKeyPassword(remoteHost.PrivateKeyPassword, opts) | ||||||
|  |  | ||||||
| 		if err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if remoteHost.PrivateKeyPassword == "" { | 		if remoteHost.PrivateKeyPassword == "" { | ||||||
|  |  | ||||||
| @@ -207,14 +208,13 @@ func (remoteHost *Host) GetAuthMethods(opts *ConfigOpts) error { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if remoteHost.Password == "" { | 	if remoteHost.Password != "" { | ||||||
|  |  | ||||||
| 		remoteHost.Password, err = GetPassword(remoteHost.Password, opts, opts.Logger) | 		opts.Logger.Debug().Str("password", remoteHost.Password).Str("Host", remoteHost.Host).Send() | ||||||
|  |  | ||||||
| 		if err != nil { | 		remoteHost.Password = GetPassword(remoteHost.Password, opts) | ||||||
|  |  | ||||||
| 			return err | 		// opts.Logger.Debug().Str("actual password", remoteHost.Password).Str("Host", remoteHost.Host).Send() | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		remoteHost.ClientConfig.Auth = append(remoteHost.ClientConfig.Auth, ssh.Password(remoteHost.Password)) | 		remoteHost.ClientConfig.Auth = append(remoteHost.ClientConfig.Auth, ssh.Password(remoteHost.Password)) | ||||||
| 	} | 	} | ||||||
| @@ -231,6 +231,8 @@ func (remoteHost *Host) GetPrivateKeyFileFromConfig() { | |||||||
| 	var identityFile string | 	var identityFile string | ||||||
| 	if remoteHost.PrivateKeyPath == "" { | 	if remoteHost.PrivateKeyPath == "" { | ||||||
| 		identityFile, _ = remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "IdentityFile") | 		identityFile, _ = remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "IdentityFile") | ||||||
|  | 		// println("Identity file:", identityFile) | ||||||
|  | 		// println("Host:", remoteHost.Host) | ||||||
| 		if identityFile == "" { | 		if identityFile == "" { | ||||||
| 			identityFile, _ = remoteHost.SSHConfigFile.DefaultUserSettings.GetStrict(remoteHost.Host, "IdentityFile") | 			identityFile, _ = remoteHost.SSHConfigFile.DefaultUserSettings.GetStrict(remoteHost.Host, "IdentityFile") | ||||||
| 			if identityFile == "" { | 			if identityFile == "" { | ||||||
| @@ -242,6 +244,7 @@ func (remoteHost *Host) GetPrivateKeyFileFromConfig() { | |||||||
| 		identityFile = remoteHost.PrivateKeyPath | 		identityFile = remoteHost.PrivateKeyPath | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// println("Identity file:", identityFile) | ||||||
| 	remoteHost.PrivateKeyPath, _ = getFullPathWithHomeDir(identityFile) | 	remoteHost.PrivateKeyPath, _ = getFullPathWithHomeDir(identityFile) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -249,14 +252,13 @@ func (remoteHost *Host) GetPrivateKeyFileFromConfig() { | |||||||
| // If it is the port is searched in the SSH config file(s) | // If it is the port is searched in the SSH config file(s) | ||||||
| func (remoteHost *Host) GetPort() { | func (remoteHost *Host) GetPort() { | ||||||
| 	port := fmt.Sprintf("%d", remoteHost.Port) | 	port := fmt.Sprintf("%d", remoteHost.Port) | ||||||
| 	// port specifed? | 	// port specified? | ||||||
| 	// port will be 0 if missing from backy config | 	// port will be 0 if missing from backy config | ||||||
| 	if port == "0" { | 	if port == "0" { | ||||||
| 		port, _ = remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "Port") | 		port, _ = remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "Port") | ||||||
|  |  | ||||||
| 		if port == "" { | 		if port == "" { | ||||||
|  |  | ||||||
| 			// get port from default SSH config file |  | ||||||
| 			port = remoteHost.SSHConfigFile.DefaultUserSettings.Get(remoteHost.Host, "Port") | 			port = remoteHost.SSHConfigFile.DefaultUserSettings.Get(remoteHost.Host, "Port") | ||||||
|  |  | ||||||
| 			// set port to be default | 			// set port to be default | ||||||
| @@ -271,7 +273,6 @@ func (remoteHost *Host) GetPort() { | |||||||
|  |  | ||||||
| func (remoteHost *Host) CombineHostNameWithPort() { | func (remoteHost *Host) CombineHostNameWithPort() { | ||||||
|  |  | ||||||
| 	// if the port is already in the HostName, leave it |  | ||||||
| 	if strings.HasSuffix(remoteHost.HostName, fmt.Sprintf(":%d", remoteHost.Port)) { | 	if strings.HasSuffix(remoteHost.HostName, fmt.Sprintf(":%d", remoteHost.Port)) { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| @@ -321,95 +322,44 @@ func (remoteHost *Host) ConnectThroughBastion(log zerolog.Logger) (*ssh.Client, | |||||||
|  |  | ||||||
| // GetKnownHosts resolves the host's KnownHosts file if it is defined | // GetKnownHosts resolves the host's KnownHosts file if it is defined | ||||||
| // if not defined, the default location for this file is used | // if not defined, the default location for this file is used | ||||||
| func (remotehHost *Host) GetKnownHosts() error { | func (remoteHost *Host) GetKnownHosts() error { | ||||||
| 	var knownHostsFileErr error | 	var knownHostsFileErr error | ||||||
| 	if TS(remotehHost.KnownHostsFile) != "" { | 	if TS(remoteHost.KnownHostsFile) != "" { | ||||||
| 		remotehHost.KnownHostsFile, knownHostsFileErr = getFullPathWithHomeDir(remotehHost.KnownHostsFile) | 		remoteHost.KnownHostsFile, knownHostsFileErr = getFullPathWithHomeDir(remoteHost.KnownHostsFile) | ||||||
| 		return knownHostsFileErr | 		return knownHostsFileErr | ||||||
| 	} | 	} | ||||||
| 	remotehHost.KnownHostsFile, knownHostsFileErr = getFullPathWithHomeDir("~/.ssh/known_hosts") | 	remoteHost.KnownHostsFile, knownHostsFileErr = getFullPathWithHomeDir("~/.ssh/known_hosts") | ||||||
| 	return knownHostsFileErr | 	return knownHostsFileErr | ||||||
| } | } | ||||||
|  |  | ||||||
| func GetPrivateKeyPassword(key string, opts *ConfigOpts, log zerolog.Logger) (string, error) { | func GetPrivateKeyPassword(key string, opts *ConfigOpts) string { | ||||||
|  | 	return getExternalConfigDirectiveValue(key, opts, AllowedExternalDirectiveAll) | ||||||
| 	var prKeyPassword string |  | ||||||
| 	if strings.HasPrefix(key, "file:") { |  | ||||||
| 		privKeyPassFilePath := strings.TrimPrefix(key, "file:") |  | ||||||
| 		privKeyPassFilePath, _ = getFullPathWithHomeDir(privKeyPassFilePath) |  | ||||||
| 		keyFile, keyFileErr := os.Open(privKeyPassFilePath) |  | ||||||
| 		if keyFileErr != nil { |  | ||||||
| 			return "", errors.Errorf("Private key password file %s failed to open. \n Make sure it is accessible and correct.", privKeyPassFilePath) |  | ||||||
| 		} |  | ||||||
| 		passwordScanner := bufio.NewScanner(keyFile) |  | ||||||
| 		for passwordScanner.Scan() { |  | ||||||
| 			prKeyPassword = passwordScanner.Text() |  | ||||||
| 		} |  | ||||||
| 	} else if strings.HasPrefix(key, "env:") { |  | ||||||
| 		privKey := strings.TrimPrefix(key, "env:") |  | ||||||
| 		privKey = strings.TrimPrefix(privKey, "${") |  | ||||||
| 		privKey = strings.TrimSuffix(privKey, "}") |  | ||||||
| 		privKey = strings.TrimPrefix(privKey, "$") |  | ||||||
| 		prKeyPassword = os.Getenv(privKey) |  | ||||||
| 	} else { |  | ||||||
| 		prKeyPassword = key |  | ||||||
| 	} |  | ||||||
| 	prKeyPassword = GetVaultKey(prKeyPassword, opts, opts.Logger) |  | ||||||
| 	return prKeyPassword, nil |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // GetPassword gets any password | // GetPassword gets any password | ||||||
| func GetPassword(pass string, opts *ConfigOpts, log zerolog.Logger) (string, error) { | func GetPassword(pass string, opts *ConfigOpts) string { | ||||||
|  | 	return getExternalConfigDirectiveValue(pass, opts, AllowedExternalDirectiveAll) | ||||||
| 	pass = strings.TrimSpace(pass) |  | ||||||
| 	if pass == "" { |  | ||||||
| 		return "", nil |  | ||||||
| 	} |  | ||||||
| 	var password string |  | ||||||
| 	if strings.HasPrefix(pass, "file:") { |  | ||||||
| 		passFilePath := strings.TrimPrefix(pass, "file:") |  | ||||||
| 		passFilePath, _ = getFullPathWithHomeDir(passFilePath) |  | ||||||
| 		keyFile, keyFileErr := os.Open(passFilePath) |  | ||||||
| 		if keyFileErr != nil { |  | ||||||
| 			return "", errors.New("Password file failed to open") |  | ||||||
| 		} |  | ||||||
| 		passwordScanner := bufio.NewScanner(keyFile) |  | ||||||
| 		for passwordScanner.Scan() { |  | ||||||
| 			password = passwordScanner.Text() |  | ||||||
| 		} |  | ||||||
| 	} else if strings.HasPrefix(pass, "env:") { |  | ||||||
| 		passEnv := strings.TrimPrefix(pass, "env:") |  | ||||||
| 		passEnv = strings.TrimPrefix(passEnv, "${") |  | ||||||
| 		passEnv = strings.TrimSuffix(passEnv, "}") |  | ||||||
| 		passEnv = strings.TrimPrefix(passEnv, "$") |  | ||||||
| 		password = os.Getenv(passEnv) |  | ||||||
| 	} else { |  | ||||||
| 		password = pass |  | ||||||
| 	} |  | ||||||
| 	password = GetVaultKey(password, opts, opts.Logger) |  | ||||||
|  |  | ||||||
| 	return password, nil |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func (remoteConfig *Host) GetProxyJumpFromConfig(hosts map[string]*Host) error { | func (remoteHost *Host) GetProxyJumpFromConfig(hosts map[string]*Host) error { | ||||||
|  |  | ||||||
| 	proxyJump, _ := remoteConfig.SSHConfigFile.SshConfigFile.Get(remoteConfig.Host, "ProxyJump") | 	proxyJump, _ := remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "ProxyJump") | ||||||
| 	if proxyJump == "" { | 	if proxyJump == "" { | ||||||
| 		proxyJump = remoteConfig.SSHConfigFile.DefaultUserSettings.Get(remoteConfig.Host, "ProxyJump") | 		proxyJump = remoteHost.SSHConfigFile.DefaultUserSettings.Get(remoteHost.Host, "ProxyJump") | ||||||
| 	} | 	} | ||||||
| 	if remoteConfig.ProxyJump == "" && proxyJump != "" { | 	if remoteHost.ProxyJump == "" && proxyJump != "" { | ||||||
| 		remoteConfig.ProxyJump = proxyJump | 		remoteHost.ProxyJump = proxyJump | ||||||
| 	} | 	} | ||||||
| 	proxyJumpHosts := strings.Split(remoteConfig.ProxyJump, ",") | 	proxyJumpHosts := strings.Split(remoteHost.ProxyJump, ",") | ||||||
| 	if remoteConfig.ProxyHost == nil && len(proxyJumpHosts) == 1 { | 	if remoteHost.ProxyHost == nil && len(proxyJumpHosts) == 1 { | ||||||
| 		remoteConfig.ProxyJump = proxyJump | 		remoteHost.ProxyJump = proxyJump | ||||||
| 		proxyHost, proxyHostFound := hosts[proxyJump] | 		proxyHost, proxyHostFound := hosts[proxyJump] | ||||||
| 		if proxyHostFound { | 		if proxyHostFound { | ||||||
| 			remoteConfig.ProxyHost = append(remoteConfig.ProxyHost, proxyHost) | 			remoteHost.ProxyHost = append(remoteHost.ProxyHost, proxyHost) | ||||||
| 		} else { | 		} else { | ||||||
| 			if proxyJump != "" { | 			if proxyJump != "" { | ||||||
| 				newProxy := &Host{Host: proxyJump} | 				newProxy := &Host{Host: proxyJump} | ||||||
| 				remoteConfig.ProxyHost = append(remoteConfig.ProxyHost, newProxy) | 				remoteHost.ProxyHost = append(remoteHost.ProxyHost, newProxy) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @@ -417,25 +367,25 @@ func (remoteConfig *Host) GetProxyJumpFromConfig(hosts map[string]*Host) error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (remoteConfig *Host) GetProxyJumpConfig(hosts map[string]*Host, opts *ConfigOpts) error { | func (remoteHost *Host) GetProxyJumpConfig(hosts map[string]*Host, opts *ConfigOpts) error { | ||||||
|  |  | ||||||
| 	if TS(remoteConfig.ConfigFilePath) == "" { | 	if TS(remoteHost.ConfigFilePath) == "" { | ||||||
| 		remoteConfig.useDefaultConfig = true | 		remoteHost.useDefaultConfig = true | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	khPathErr := remoteConfig.GetKnownHosts() | 	khPathErr := remoteHost.GetKnownHosts() | ||||||
|  |  | ||||||
| 	if khPathErr != nil { | 	if khPathErr != nil { | ||||||
| 		return khPathErr | 		return khPathErr | ||||||
| 	} | 	} | ||||||
| 	if remoteConfig.ClientConfig == nil { | 	if remoteHost.ClientConfig == nil { | ||||||
| 		remoteConfig.ClientConfig = &ssh.ClientConfig{} | 		remoteHost.ClientConfig = &ssh.ClientConfig{} | ||||||
| 	} | 	} | ||||||
| 	var configFile *os.File | 	var configFile *os.File | ||||||
| 	var sshConfigFileOpenErr error | 	var sshConfigFileOpenErr error | ||||||
| 	if !remoteConfig.useDefaultConfig { | 	if !remoteHost.useDefaultConfig { | ||||||
|  |  | ||||||
| 		configFile, sshConfigFileOpenErr = os.Open(remoteConfig.ConfigFilePath) | 		configFile, sshConfigFileOpenErr = os.Open(remoteHost.ConfigFilePath) | ||||||
| 		if sshConfigFileOpenErr != nil { | 		if sshConfigFileOpenErr != nil { | ||||||
| 			return sshConfigFileOpenErr | 			return sshConfigFileOpenErr | ||||||
| 		} | 		} | ||||||
| @@ -446,39 +396,39 @@ func (remoteConfig *Host) GetProxyJumpConfig(hosts map[string]*Host, opts *Confi | |||||||
| 			return sshConfigFileOpenErr | 			return sshConfigFileOpenErr | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	remoteConfig.SSHConfigFile = &sshConfigFile{} | 	remoteHost.SSHConfigFile = &sshConfigFile{} | ||||||
| 	remoteConfig.SSHConfigFile.DefaultUserSettings = ssh_config.DefaultUserSettings | 	remoteHost.SSHConfigFile.DefaultUserSettings = ssh_config.DefaultUserSettings | ||||||
| 	var decodeErr error | 	var decodeErr error | ||||||
| 	remoteConfig.SSHConfigFile.SshConfigFile, decodeErr = ssh_config.Decode(configFile) | 	remoteHost.SSHConfigFile.SshConfigFile, decodeErr = ssh_config.Decode(configFile) | ||||||
| 	if decodeErr != nil { | 	if decodeErr != nil { | ||||||
| 		return decodeErr | 		return decodeErr | ||||||
| 	} | 	} | ||||||
| 	remoteConfig.GetPrivateKeyFileFromConfig() | 	remoteHost.GetPrivateKeyFileFromConfig() | ||||||
| 	remoteConfig.GetPort() | 	remoteHost.GetPort() | ||||||
| 	remoteConfig.GetHostName() | 	remoteHost.GetHostName() | ||||||
| 	remoteConfig.CombineHostNameWithPort() | 	remoteHost.CombineHostNameWithPort() | ||||||
| 	remoteConfig.GetSshUserFromConfig() | 	remoteHost.GetSshUserFromConfig() | ||||||
| 	remoteConfig.isProxyHost = true | 	remoteHost.isProxyHost = true | ||||||
| 	if remoteConfig.HostName == "" { | 	if remoteHost.HostName == "" { | ||||||
| 		return errors.Errorf("No hostname found or specified for host %s", remoteConfig.Host) | 		return errors.Errorf("No hostname found or specified for host %s", remoteHost.Host) | ||||||
| 	} | 	} | ||||||
| 	err := remoteConfig.GetAuthMethods(opts) | 	err := remoteHost.GetAuthMethods(opts) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// TODO: Add value/option to config for host key and add bool to check for host key | 	// TODO: Add value/option to config for host key and add bool to check for host key | ||||||
| 	hostKeyCallback, err := knownhosts.New(remoteConfig.KnownHostsFile) | 	hostKeyCallback, err := knownhosts.New(remoteHost.KnownHostsFile) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("could not create hostkeycallback function: %v", err) | 		return fmt.Errorf("could not create hostkeycallback function: %v", err) | ||||||
| 	} | 	} | ||||||
| 	remoteConfig.ClientConfig.HostKeyCallback = hostKeyCallback | 	remoteHost.ClientConfig.HostKeyCallback = hostKeyCallback | ||||||
| 	hosts[remoteConfig.Host] = remoteConfig | 	hosts[remoteHost.Host] = remoteHost | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([]string, error) { | func (command *Command) RunCmdOnHost(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([]string, error) { | ||||||
| 	var ( | 	var ( | ||||||
| 		ArgsStr       string | 		ArgsStr       string | ||||||
| 		cmdOutBuf     bytes.Buffer | 		cmdOutBuf     bytes.Buffer | ||||||
| @@ -489,7 +439,6 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) | |||||||
| 			env:  command.Environment, | 			env:  command.Environment, | ||||||
| 		} | 		} | ||||||
| 	) | 	) | ||||||
| 	// Getting the command type must be done before concatenating the arguments |  | ||||||
| 	command = getCommandTypeAndSetCommandInfo(command) | 	command = getCommandTypeAndSetCommandInfo(command) | ||||||
|  |  | ||||||
| 	// Prepare command arguments | 	// Prepare command arguments | ||||||
| @@ -499,8 +448,8 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) | |||||||
|  |  | ||||||
| 	cmdCtxLogger.Info(). | 	cmdCtxLogger.Info(). | ||||||
| 		Str("Command", command.Name). | 		Str("Command", command.Name). | ||||||
| 		Str("Host", *command.Host). | 		Str("Host", command.Host). | ||||||
| 		Msgf("Running %s on host %s", getCommandTypeAndSetCommandInfoLabel(command.Type), *command.Host) | 		Msgf("Running %s on host %s", getCommandTypeAndSetCommandInfoLabel(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() | ||||||
|  |  | ||||||
| @@ -531,33 +480,15 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) | |||||||
|  |  | ||||||
| 	// Handle command execution based on type | 	// Handle command execution based on type | ||||||
| 	switch command.Type { | 	switch command.Type { | ||||||
| 	case ScriptCT: | 	case ScriptCommandType: | ||||||
| 		return command.runScript(commandSession, cmdCtxLogger, &cmdOutBuf) | 		return command.runScript(commandSession, cmdCtxLogger, &cmdOutBuf) | ||||||
| 	case RemoteScriptCT: | 	case RemoteScriptCommandType: | ||||||
| 		return command.runRemoteScript(commandSession, cmdCtxLogger, &cmdOutBuf) | 		return command.runRemoteScript(commandSession, cmdCtxLogger, &cmdOutBuf) | ||||||
| 	case ScriptFileCT: | 	case ScriptFileCommandType: | ||||||
| 		return command.runScriptFile(commandSession, cmdCtxLogger, &cmdOutBuf) | 		return command.runScriptFile(commandSession, cmdCtxLogger, &cmdOutBuf) | ||||||
| 	case PackageCT: | 	case PackageCommandType: | ||||||
| 		if command.PackageOperation == PackOpCheckVersion { | 		var remoteHostPackageExecutor RemoteHostPackageExecutor | ||||||
| 			commandSession.Stderr = nil | 		return remoteHostPackageExecutor.RunCmdOnHost(command, commandSession, cmdCtxLogger, cmdOutBuf) | ||||||
| 			// 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, command.OutputToLog), 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) | ||||||
| @@ -565,17 +496,107 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) | |||||||
| 			ArgsStr = fmt.Sprintf("%s %s", command.Cmd, ArgsStr) | 			ArgsStr = fmt.Sprintf("%s %s", command.Cmd, ArgsStr) | ||||||
| 		} | 		} | ||||||
| 		cmdCtxLogger.Debug().Str("cmd + args", ArgsStr).Send() | 		cmdCtxLogger.Debug().Str("cmd + args", ArgsStr).Send() | ||||||
| 		// Run simple command |  | ||||||
|  | 		if command.Type == UserCommandType && command.UserOperation == "password" { | ||||||
|  | 			// cmdCtxLogger.Debug().Msgf("adding stdin") | ||||||
|  |  | ||||||
|  | 			userNamePass := fmt.Sprintf("%s:%s", command.Username, command.UserPassword) | ||||||
|  | 			client, err := sftp.NewClient(command.RemoteHost.SshClient) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error creating sftp client: %v", err) | ||||||
|  | 			} | ||||||
|  | 			uuidFile := uuid.New() | ||||||
|  | 			passFilePath := fmt.Sprintf("/tmp/%s", uuidFile.String()) | ||||||
|  | 			passFile, passFileErr := client.Create(passFilePath) | ||||||
|  | 			if passFileErr != nil { | ||||||
|  | 				return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error creating file /tmp/%s: %v", uuidFile.String(), passFileErr) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			_, err = passFile.Write([]byte(userNamePass)) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error writing to file /tmp/%s: %v", uuidFile.String(), err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			ArgsStr = fmt.Sprintf("cat %s | chpasswd", passFilePath) | ||||||
|  | 			defer passFile.Close() | ||||||
|  |  | ||||||
|  | 			rmFileFunc := func() { | ||||||
|  | 				_ = client.Remove(passFilePath) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			defer rmFileFunc() | ||||||
|  | 		} | ||||||
| 		if err := commandSession.Run(ArgsStr); err != nil { | 		if err := commandSession.Run(ArgsStr); err != nil { | ||||||
| 			return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), fmt.Errorf("error running command: %w", err) | 			return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error running command: %w", err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if command.Type == UserCommandType { | ||||||
|  |  | ||||||
|  | 			// REFACTOR IF/WHEN WINDOWS SUPPORT IS ADDED | ||||||
|  |  | ||||||
|  | 			if command.UserOperation == "add" { | ||||||
|  | 				if command.UserSshPubKeys != nil { | ||||||
|  | 					var ( | ||||||
|  | 						f        *sftp.File | ||||||
|  | 						err      error | ||||||
|  | 						userHome []byte | ||||||
|  | 						client   *sftp.Client | ||||||
|  | 					) | ||||||
|  |  | ||||||
|  | 					cmdCtxLogger.Info().Msg("adding SSH Keys") | ||||||
|  |  | ||||||
|  | 					commandSession, _ = command.RemoteHost.createSSHSession(opts) | ||||||
|  | 					userHome, err = commandSession.CombinedOutput(fmt.Sprintf("grep \"%s\" /etc/passwd | cut -d: -f6", command.Username)) | ||||||
|  | 					if err != nil { | ||||||
|  | 						return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error finding user home from /etc/passwd: %v", err) | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					command.UserHome = strings.TrimSpace(string(userHome)) | ||||||
|  | 					userSshDir := fmt.Sprintf("%s/.ssh", command.UserHome) | ||||||
|  | 					client, err = sftp.NewClient(command.RemoteHost.SshClient) | ||||||
|  | 					if err != nil { | ||||||
|  | 						return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error creating sftp client: %v", err) | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					err = client.MkdirAll(userSshDir) | ||||||
|  | 					if err != nil { | ||||||
|  | 						return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error creating directory %s: %v", userSshDir, err) | ||||||
|  | 					} | ||||||
|  | 					_, err = client.Create(fmt.Sprintf("%s/authorized_keys", userSshDir)) | ||||||
|  | 					if err != nil { | ||||||
|  | 						return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error opening file %s/authorized_keys: %v", userSshDir, err) | ||||||
|  | 					} | ||||||
|  | 					f, err = client.OpenFile(fmt.Sprintf("%s/authorized_keys", userSshDir), os.O_APPEND|os.O_CREATE|os.O_WRONLY) | ||||||
|  | 					if err != nil { | ||||||
|  | 						return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error opening file %s/authorized_keys: %v", userSshDir, err) | ||||||
|  | 					} | ||||||
|  | 					defer f.Close() | ||||||
|  | 					for _, k := range command.UserSshPubKeys { | ||||||
|  | 						buf := bytes.NewBufferString(k) | ||||||
|  | 						cmdCtxLogger.Info().Str("key", k).Msg("adding SSH key") | ||||||
|  | 						if _, err := f.ReadFrom(buf); err != nil { | ||||||
|  | 							return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error adding to authorized keys: %v", err) | ||||||
| 						} | 						} | ||||||
| 					} | 					} | ||||||
|  |  | ||||||
| 	return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), nil | 					commandSession, _ = command.RemoteHost.createSSHSession(opts) | ||||||
|  | 					_, err = commandSession.CombinedOutput(fmt.Sprintf("chown -R %s:%s %s", command.Username, command.Username, userHome)) | ||||||
|  | 					if err != nil { | ||||||
|  | 						return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), err | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func checkPackageVersion(cmdCtxLogger zerolog.Logger, command *Command, commandSession *ssh.Session, cmdOutBuf bytes.Buffer) ([]string, error) { | 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") | 	for _, p := range command.Packages { | ||||||
|  | 		cmdCtxLogger.Info().Str("package", p.Name).Msg("Checking package versions") | ||||||
|  | 	} | ||||||
| 	// Prepare command arguments | 	// Prepare command arguments | ||||||
| 	ArgsStr := command.Cmd | 	ArgsStr := command.Cmd | ||||||
| 	for _, v := range command.Args { | 	for _, v := range command.Args { | ||||||
| @@ -590,9 +611,9 @@ func checkPackageVersion(cmdCtxLogger zerolog.Logger, command *Command, commandS | |||||||
|  |  | ||||||
| 		_, parseErr := parsePackageVersion(string(cmdOut), cmdCtxLogger, command, cmdOutBuf) | 		_, parseErr := parsePackageVersion(string(cmdOut), cmdCtxLogger, command, cmdOutBuf) | ||||||
| 		if parseErr != nil { | 		if parseErr != nil { | ||||||
| 			return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), fmt.Errorf("error: package %s not listed: %w", command.PackageName, err) | 			return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error: packages %v not listed: %w", command.Packages, err) | ||||||
| 		} | 		} | ||||||
| 		return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), fmt.Errorf("error running %s: %w", ArgsStr, err) | 		return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error running %s: %w", ArgsStr, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return parsePackageVersion(string(cmdOut), cmdCtxLogger, command, cmdOutBuf) | 	return parsePackageVersion(string(cmdOut), cmdCtxLogger, command, cmdOutBuf) | ||||||
| @@ -622,7 +643,7 @@ func (command *Command) runScript(session *ssh.Session, cmdCtxLogger zerolog.Log | |||||||
| 		return collectOutput(outputBuf, command.Name, cmdCtxLogger, true), 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, command.GetOutput), nil | 	return collectOutput(outputBuf, command.Name, cmdCtxLogger, command.Output.ToLog), nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // runScriptFile handles the execution of script files. | // runScriptFile handles the execution of script files. | ||||||
| @@ -641,7 +662,7 @@ func (command *Command) runScriptFile(session *ssh.Session, cmdCtxLogger zerolog | |||||||
| 		return collectOutput(outputBuf, command.Name, cmdCtxLogger, true), 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, command.OutputToLog), nil | 	return collectOutput(outputBuf, command.Name, cmdCtxLogger, command.Output.ToLog), nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // prepareScriptBuffer prepares a buffer for inline scripts. | // prepareScriptBuffer prepares a buffer for inline scripts. | ||||||
| @@ -698,10 +719,10 @@ func (command *Command) runRemoteScript(session *ssh.Session, cmdCtxLogger zerol | |||||||
| 	err = session.Run(command.Shell) | 	err = session.Run(command.Shell) | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return collectOutput(outputBuf, command.Name, cmdCtxLogger, command.OutputToLog), fmt.Errorf("error running remote script: %w", err) | 		return collectOutput(outputBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error running remote script: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return collectOutput(outputBuf, command.Name, cmdCtxLogger, command.OutputToLog), nil | 	return collectOutput(outputBuf, command.Name, cmdCtxLogger, command.Output.ToLog), nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // readFileToBuffer reads a file into a buffer. | // readFileToBuffer reads a file into a buffer. | ||||||
| @@ -774,7 +795,7 @@ func (h *Host) DetectOS(opts *ConfigOpts) (string, error) { | |||||||
| 	return osName, nil | 	return osName, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func CheckIfHostHasHostName(host string) (bool, string) { | func DoesHostHaveHostName(host string) (bool, string) { | ||||||
| 	HostName, err := ssh_config.DefaultUserSettings.GetStrict(host, "HostName") | 	HostName, err := ssh_config.DefaultUserSettings.GetStrict(host, "HostName") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, "" | 		return false, "" | ||||||
| @@ -782,3 +803,39 @@ func CheckIfHostHasHostName(host string) (bool, string) { | |||||||
| 	println(HostName) | 	println(HostName) | ||||||
| 	return HostName != "", HostName | 	return HostName != "", HostName | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func IsHostLocal(host string) bool { | ||||||
|  | 	host = strings.ToLower(host) | ||||||
|  | 	return host == "127.0.0.1" || host == "localhost" || host == "" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type RemoteHostPackageExecutor struct{} | ||||||
|  |  | ||||||
|  | func (r RemoteHostPackageExecutor) RunCmdOnHost(command *Command, commandSession *ssh.Session, cmdCtxLogger zerolog.Logger, cmdOutBuf bytes.Buffer) ([]string, error) { | ||||||
|  | 	var ArgsStr string | ||||||
|  | 	// Prepare command arguments | ||||||
|  | 	for _, v := range command.Args { | ||||||
|  | 		ArgsStr += fmt.Sprintf(" %s", v) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if command.PackageOperation == PackageOperationCheckVersion { | ||||||
|  | 		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) | ||||||
|  | 	} | ||||||
|  | 	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, command.Output.ToLog), fmt.Errorf("error running command: %w", err) | ||||||
|  | 	} | ||||||
|  | 	return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), nil | ||||||
|  | } | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman" | 	"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman" | ||||||
|  | 	packagemanagercommon "git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/common" | ||||||
| 	"git.andrewnw.xyz/CyberShell/backy/pkg/remotefetcher" | 	"git.andrewnw.xyz/CyberShell/backy/pkg/remotefetcher" | ||||||
| 	"git.andrewnw.xyz/CyberShell/backy/pkg/usermanager" | 	"git.andrewnw.xyz/CyberShell/backy/pkg/usermanager" | ||||||
| 	vaultapi "github.com/hashicorp/vault/api" | 	vaultapi "github.com/hashicorp/vault/api" | ||||||
| @@ -26,15 +27,15 @@ type ( | |||||||
| 		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"` | ||||||
| 		KnownHostsFile     string `yaml:"knownhostsfile,omitempty"` | 		KnownHostsFile     string `yaml:"knownHostsFile,omitempty"` | ||||||
| 		ClientConfig       *ssh.ClientConfig | 		ClientConfig       *ssh.ClientConfig | ||||||
| 		SSHConfigFile      *sshConfigFile | 		SSHConfigFile      *sshConfigFile | ||||||
| 		SshClient          *ssh.Client | 		SshClient          *ssh.Client | ||||||
| 		Port               uint16 `yaml:"port,omitempty"` | 		Port               uint16 `yaml:"port,omitempty"` | ||||||
| 		ProxyJump          string `yaml:"proxyjump,omitempty"` | 		ProxyJump          string `yaml:"proxyjump,omitempty"` | ||||||
| 		Password           string `yaml:"password,omitempty"` | 		Password           string `yaml:"password,omitempty"` | ||||||
| 		PrivateKeyPath     string `yaml:"privatekeypath,omitempty"` | 		PrivateKeyPath     string `yaml:"IdentityFile,omitempty"` | ||||||
| 		PrivateKeyPassword string `yaml:"privatekeypassword,omitempty"` | 		PrivateKeyPassword string `yaml:"privateKeyPassword,omitempty"` | ||||||
| 		useDefaultConfig   bool | 		useDefaultConfig   bool | ||||||
| 		User               string `yaml:"user,omitempty"` | 		User               string `yaml:"user,omitempty"` | ||||||
| 		isProxyHost        bool | 		isProxyHost        bool | ||||||
| @@ -51,61 +52,45 @@ type ( | |||||||
| 	Command struct { | 	Command struct { | ||||||
| 		Name string `yaml:"name,omitempty"` | 		Name string `yaml:"name,omitempty"` | ||||||
|  |  | ||||||
| 		// command to run |  | ||||||
| 		Cmd string `yaml:"cmd"` | 		Cmd string `yaml:"cmd"` | ||||||
|  |  | ||||||
| 		// See CommandType enum further down the page for acceptable values | 		// See CommandType enum further down the page for acceptable values | ||||||
| 		Type CommandType `yaml:"type,omitempty"` | 		Type CommandType `yaml:"type,omitempty"` | ||||||
|  |  | ||||||
| 		// host on which to run cmd | 		Host string `yaml:"host,omitempty"` | ||||||
| 		Host *string `yaml:"host,omitempty"` |  | ||||||
|  |  | ||||||
| 		// Hooks are for running commands on certain events |  | ||||||
| 		Hooks *Hooks `yaml:"hooks,omitempty"` | 		Hooks *Hooks `yaml:"hooks,omitempty"` | ||||||
|  |  | ||||||
| 		// hook refs are internal references of commands for each hook type |  | ||||||
| 		hookRefs map[string]map[string]*Command | 		hookRefs map[string]map[string]*Command | ||||||
|  |  | ||||||
| 		// Shell specifies which shell to run the command in, if any. |  | ||||||
| 		Shell string `yaml:"shell,omitempty"` | 		Shell string `yaml:"shell,omitempty"` | ||||||
|  |  | ||||||
| 		RemoteHost *Host `yaml:"-"` | 		RemoteHost *Host `yaml:"-"` | ||||||
|  |  | ||||||
| 		// Args is an array that holds the arguments to cmd |  | ||||||
| 		Args []string `yaml:"args,omitempty"` | 		Args []string `yaml:"args,omitempty"` | ||||||
|  |  | ||||||
| 		/* |  | ||||||
| 			Dir specifies a directory in which to run the command. |  | ||||||
| 		*/ |  | ||||||
| 		Dir *string `yaml:"dir,omitempty"` | 		Dir *string `yaml:"dir,omitempty"` | ||||||
|  |  | ||||||
| 		// Env points to a file containing env variables to be used with the command |  | ||||||
| 		Env string `yaml:"env,omitempty"` | 		Env string `yaml:"env,omitempty"` | ||||||
|  |  | ||||||
| 		// Environment holds env variables to be used with the command |  | ||||||
| 		Environment []string `yaml:"environment,omitempty"` | 		Environment []string `yaml:"environment,omitempty"` | ||||||
|  |  | ||||||
| 		// Output determines if output is requested. |  | ||||||
| 		// |  | ||||||
| 		// Only for when command is in a list. |  | ||||||
| 		GetOutput bool `yaml:"getOutput,omitempty"` |  | ||||||
|  |  | ||||||
| 		ScriptEnvFile string `yaml:"scriptEnvFile"` | 		ScriptEnvFile string `yaml:"scriptEnvFile"` | ||||||
|  |  | ||||||
| 		OutputToLog bool `yaml:"outputToLog,omitempty"` | 		Output struct { | ||||||
|  | 			File   string `yaml:"file,omitempty"` | ||||||
| 		OutputFile string `yaml:"outputFile,omitempty"` | 			ToLog  bool   `yaml:"toLog,omitempty"` | ||||||
|  | 			InList bool   `yaml:"inList,omitempty"` | ||||||
|  | 		} `yaml:"output"` | ||||||
|  |  | ||||||
| 		// BEGIN PACKAGE COMMAND FIELDS | 		// BEGIN PACKAGE COMMAND FIELDS | ||||||
|  |  | ||||||
| 		PackageManager string `yaml:"packageManager,omitempty"` | 		PackageManager string `yaml:"packageManager,omitempty"` | ||||||
|  |  | ||||||
| 		PackageName string `yaml:"packageName,omitempty"` | 		Packages []packagemanagercommon.Package `yaml:"packages,omitempty"` | ||||||
|  |  | ||||||
| 		// Version specifies the desired version for package execution |  | ||||||
| 		PackageVersion string `yaml:"packageVersion,omitempty"` | 		PackageVersion string `yaml:"packageVersion,omitempty"` | ||||||
|  |  | ||||||
| 		// PackageOperation specifies the action for package-related commands (e.g., "install" or "remove") |  | ||||||
| 		PackageOperation PackageOperation `yaml:"packageOperation,omitempty"` | 		PackageOperation PackageOperation `yaml:"packageOperation,omitempty"` | ||||||
|  |  | ||||||
| 		pkgMan pkgman.PackageManager | 		pkgMan pkgman.PackageManager | ||||||
| @@ -113,42 +98,37 @@ type ( | |||||||
| 		packageCmdSet bool | 		packageCmdSet bool | ||||||
| 		// END PACKAGE COMMAND FIELDS | 		// END PACKAGE COMMAND FIELDS | ||||||
|  |  | ||||||
| 		// RemoteSource specifies a URL to fetch the command or configuration remotely |  | ||||||
| 		RemoteSource string `yaml:"remoteSource,omitempty"` | 		RemoteSource string `yaml:"remoteSource,omitempty"` | ||||||
|  |  | ||||||
| 		// FetchBeforeExecution determines if the remoteSource should be fetched before running |  | ||||||
| 		FetchBeforeExecution bool `yaml:"fetchBeforeExecution,omitempty"` | 		FetchBeforeExecution bool `yaml:"fetchBeforeExecution,omitempty"` | ||||||
|  |  | ||||||
| 		Fetcher remotefetcher.RemoteFetcher | 		Fetcher remotefetcher.RemoteFetcher | ||||||
|  |  | ||||||
| 		// BEGIN USER COMMAND FIELDS | 		// BEGIN USER COMMAND FIELDS | ||||||
|  |  | ||||||
| 		// Username specifies the username for user creation or related operations |  | ||||||
| 		Username string `yaml:"userName,omitempty"` | 		Username string `yaml:"userName,omitempty"` | ||||||
|  |  | ||||||
| 		UserID string `yaml:"userID,omitempty"` | 		UserID string `yaml:"userID,omitempty"` | ||||||
|  |  | ||||||
| 		// UserGroups specifies the groups to add the user to |  | ||||||
| 		UserGroups []string `yaml:"userGroups,omitempty"` | 		UserGroups []string `yaml:"userGroups,omitempty"` | ||||||
|  |  | ||||||
| 		// UserHome specifies the home directory for the user |  | ||||||
| 		UserHome string `yaml:"userHome,omitempty"` | 		UserHome string `yaml:"userHome,omitempty"` | ||||||
|  |  | ||||||
| 		// UserShell specifies the shell for the user |  | ||||||
| 		UserShell string `yaml:"userShell,omitempty"` | 		UserShell string `yaml:"userShell,omitempty"` | ||||||
|  |  | ||||||
| 		// SystemUser specifies whether the user is a system account | 		UserCreateHome bool `yaml:"userCreateHome,omitempty"` | ||||||
| 		SystemUser bool `yaml:"systemUser,omitempty"` |  | ||||||
|  | 		UserIsSystem bool `yaml:"userIsSystem,omitempty"` | ||||||
|  |  | ||||||
| 		// UserPassword specifies the password for the user (can be file: or plain text) |  | ||||||
| 		UserPassword string `yaml:"userPassword,omitempty"` | 		UserPassword string `yaml:"userPassword,omitempty"` | ||||||
|  |  | ||||||
|  | 		UserSshPubKeys []string `yaml:"userSshPubKeys,omitempty"` | ||||||
|  |  | ||||||
| 		userMan usermanager.UserManager | 		userMan usermanager.UserManager | ||||||
|  |  | ||||||
| 		// OS for the command, only used when type is user | 		// OS for the command, only used when type is user | ||||||
| 		OS string `yaml:"OS,omitempty"` | 		OS string `yaml:"OS,omitempty"` | ||||||
|  |  | ||||||
| 		// UserOperation specifies the action for user-related commands (e.g., "create" or "remove") |  | ||||||
| 		UserOperation string `yaml:"userOperation,omitempty"` | 		UserOperation string `yaml:"userOperation,omitempty"` | ||||||
|  |  | ||||||
| 		userCmdSet bool | 		userCmdSet bool | ||||||
| @@ -156,7 +136,7 @@ type ( | |||||||
| 		// stdin only for userOperation = password (for now) | 		// stdin only for userOperation = password (for now) | ||||||
| 		stdin *strings.Reader | 		stdin *strings.Reader | ||||||
|  |  | ||||||
| 		// END USER STRUCT FIELDS | 		// END USER STRUCommandType FIELDS | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	RemoteSource struct { | 	RemoteSource struct { | ||||||
| @@ -176,8 +156,12 @@ type ( | |||||||
| 		RunCmdOnFailure                          string   `yaml:"runCmdOnFailure,omitempty"` | 		RunCmdOnFailure                          string   `yaml:"runCmdOnFailure,omitempty"` | ||||||
| 		Order                                    []string `yaml:"order,omitempty"` | 		Order                                    []string `yaml:"order,omitempty"` | ||||||
| 		Notifications                            []string `yaml:"notifications,omitempty"` | 		Notifications                            []string `yaml:"notifications,omitempty"` | ||||||
| 		GetOutput       bool     `yaml:"getOutput,omitempty"` | 		GetCommandOutputInNotificationsOnSuccess bool     `yaml:"sendNotificationOnSuccess,omitempty"` | ||||||
| 		NotifyOnSuccess bool     `yaml:"notifyOnSuccess,omitempty"` |  | ||||||
|  | 		Notify struct { | ||||||
|  | 			OnFailure bool `yaml:"onFailure,omitempty"` | ||||||
|  | 			OnSuccess bool `yaml:"onSuccess,omitempty"` | ||||||
|  | 		} `yaml:"notify,omitempty"` | ||||||
|  |  | ||||||
| 		NotifyConfig *notify.Notify | 		NotifyConfig *notify.Notify | ||||||
| 		Source       string `yaml:"source"` // URL to fetch remote commands | 		Source       string `yaml:"source"` // URL to fetch remote commands | ||||||
| @@ -206,6 +190,8 @@ type ( | |||||||
|  |  | ||||||
| 		ConfigFilePath string | 		ConfigFilePath string | ||||||
|  |  | ||||||
|  | 		HostsFilePath string | ||||||
|  |  | ||||||
| 		ConfigDir string | 		ConfigDir string | ||||||
|  |  | ||||||
| 		LogFilePath string | 		LogFilePath string | ||||||
| @@ -226,6 +212,8 @@ type ( | |||||||
|  |  | ||||||
| 		List ListConfig | 		List ListConfig | ||||||
|  |  | ||||||
|  | 		Vars map[string]string `yaml:"variables"` | ||||||
|  |  | ||||||
| 		VaultKeys []*VaultKey `yaml:"keys"` | 		VaultKeys []*VaultKey `yaml:"keys"` | ||||||
|  |  | ||||||
| 		koanf *koanf.Koanf | 		koanf *koanf.Koanf | ||||||
| @@ -244,6 +232,7 @@ type ( | |||||||
|  |  | ||||||
| 	VaultKey struct { | 	VaultKey struct { | ||||||
| 		Name      string `yaml:"name"` | 		Name      string `yaml:"name"` | ||||||
|  | 		Key       string `yaml:"key"` | ||||||
| 		Path      string `yaml:"path"` | 		Path      string `yaml:"path"` | ||||||
| 		ValueType string `yaml:"type"` | 		ValueType string `yaml:"type"` | ||||||
| 		MountPath string `yaml:"mountpath"` | 		MountPath string `yaml:"mountpath"` | ||||||
| @@ -259,6 +248,7 @@ type ( | |||||||
| 	Notifications struct { | 	Notifications struct { | ||||||
| 		MailConfig   map[string]MailConfig   `yaml:"mail,omitempty"` | 		MailConfig   map[string]MailConfig   `yaml:"mail,omitempty"` | ||||||
| 		MatrixConfig map[string]MatrixStruct `yaml:"matrix,omitempty"` | 		MatrixConfig map[string]MatrixStruct `yaml:"matrix,omitempty"` | ||||||
|  | 		HttpConfig   map[string]HttpConfig   `yaml:"http,omitempty"` | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	CmdOutput struct { | 	CmdOutput struct { | ||||||
| @@ -294,28 +284,55 @@ type ( | |||||||
| 		Error    error  // Error encountered, if any | 		Error    error  // Error encountered, if any | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	ListMetrics struct { | ||||||
|  | 		Name                 string | ||||||
|  | 		SuccessfulExecutions uint64 | ||||||
|  | 		FailedExecutions     uint64 | ||||||
|  | 		TotalExecutions      uint64 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	CommandMetrics struct { | ||||||
|  | 		Name                 string | ||||||
|  | 		SuccessfulExecutions uint64 | ||||||
|  | 		FailedExecutions     uint64 | ||||||
|  | 		TotalExecutions      uint64 | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// use ints so we can use enums | 	// use ints so we can use enums | ||||||
| 	CommandType               int | 	CommandType               int | ||||||
| 	PackageOperation          int | 	PackageOperation          int | ||||||
|  | 	AllowedExternalDirectives int | ||||||
| ) | ) | ||||||
|  |  | ||||||
| //go:generate go run github.com/dmarkham/enumer -linecomment -yaml -text -json -type=CommandType | //go:generate go run github.com/dmarkham/enumer -linecomment -yaml -text -json -type=CommandType | ||||||
| const ( | const ( | ||||||
| 	DefaultCT      CommandType = iota // | 	DefaultCommandType      CommandType = iota // | ||||||
| 	ScriptCT                          // script | 	ScriptCommandType                          // script | ||||||
| 	ScriptFileCT                      // scriptFile | 	ScriptFileCommandType                      // scriptFile | ||||||
| 	RemoteScriptCT                    // remoteScript | 	RemoteScriptCommandType                    // remoteScript | ||||||
| 	PackageCT                         // package | 	PackageCommandType                         // package | ||||||
| 	UserCT                            // user | 	UserCommandType                            // user | ||||||
| ) | ) | ||||||
|  |  | ||||||
| //go:generate go run github.com/dmarkham/enumer -linecomment -yaml -text -json -type=PackageOperation | //go:generate go run github.com/dmarkham/enumer -linecomment -yaml -text -json -type=PackageOperation | ||||||
| const ( | const ( | ||||||
| 	DefaultPO                    PackageOperation = iota // | 	DefaultPO                    PackageOperation = iota // | ||||||
| 	PackOpInstall                              // install | 	PackageOperationInstall                              // install | ||||||
| 	PackOpUpgrade                              // upgrade | 	PackageOperationUpgrade                              // upgrade | ||||||
| 	PackOpPurge                                // purge | 	PackageOperationPurge                                // purge | ||||||
| 	PackOpRemove                               // remove | 	PackageOperationRemove                               // remove | ||||||
| 	PackOpCheckVersion                         // checkVersion | 	PackageOperationCheckVersion                         // checkVersion | ||||||
| 	PackOpIsInstalled                          // isInstalled | 	PackageOperationIsInstalled                          // isInstalled | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | //go:generate go run github.com/dmarkham/enumer -linecomment -yaml -text -json -type=AllowedExternalDirectives | ||||||
|  | const ( | ||||||
|  | 	DefaultExternalDir                AllowedExternalDirectives = iota | ||||||
|  | 	AllowedExternalDirectiveVault                               // vault | ||||||
|  | 	AllowedExternalDirectiveVaultEnv                            // vault-env | ||||||
|  | 	AllowedExternalDirectiveVaultFile                           // vault-file | ||||||
|  | 	AllowedExternalDirectiveAll                                 // vault-file-env | ||||||
|  | 	AllowedExternalDirectiveFileEnv                             // file-env | ||||||
|  | 	AllowedExternalDirectiveFile                                // file | ||||||
|  | 	AllowedExternalDirectiveEnv                                 // env | ||||||
| ) | ) | ||||||
|   | |||||||
| @@ -6,16 +6,19 @@ package backy | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
|  | 	"context" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os" | 	"os" | ||||||
| 	"os/exec" | 	"os/exec" | ||||||
| 	"path" | 	"path" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
|  | 	"regexp" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"git.andrewnw.xyz/CyberShell/backy/pkg/logging" | 	"git.andrewnw.xyz/CyberShell/backy/pkg/logging" | ||||||
| 	"git.andrewnw.xyz/CyberShell/backy/pkg/remotefetcher" | 	"git.andrewnw.xyz/CyberShell/backy/pkg/remotefetcher" | ||||||
|  | 	vault "github.com/hashicorp/vault/api" | ||||||
| 	"github.com/joho/godotenv" | 	"github.com/joho/godotenv" | ||||||
| 	"github.com/knadh/koanf/v2" | 	"github.com/knadh/koanf/v2" | ||||||
| 	"github.com/rs/zerolog" | 	"github.com/rs/zerolog" | ||||||
| @@ -65,8 +68,14 @@ func SetLogFile(logFile string) BackyOptionFunc { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // SetCmdStdOut forces the command output to stdout | func SetHostsConfigFile(hostsConfigFile string) BackyOptionFunc { | ||||||
| func SetCmdStdOut(setStdOut bool) BackyOptionFunc { | 	return func(bco *ConfigOpts) { | ||||||
|  | 		bco.HostsFilePath = hostsConfigFile | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // EnableCommandStdOut forces the command output to stdout | ||||||
|  | func EnableCommandStdOut(setStdOut bool) BackyOptionFunc { | ||||||
| 	return func(bco *ConfigOpts) { | 	return func(bco *ConfigOpts) { | ||||||
| 		bco.CmdStdOut = setStdOut | 		bco.CmdStdOut = setStdOut | ||||||
| 	} | 	} | ||||||
| @@ -79,7 +88,7 @@ func EnableCron() BackyOptionFunc { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func NewOpts(configFilePath string, opts ...BackyOptionFunc) *ConfigOpts { | func NewConfigOptions(configFilePath string, opts ...BackyOptionFunc) *ConfigOpts { | ||||||
| 	b := &ConfigOpts{} | 	b := &ConfigOpts{} | ||||||
| 	b.ConfigFilePath = configFilePath | 	b.ConfigFilePath = configFilePath | ||||||
| 	for _, opt := range opts { | 	for _, opt := range opts { | ||||||
| @@ -108,7 +117,11 @@ func injectEnvIntoSSH(envVarsToInject environmentVars, process *ssh.Session, opt | |||||||
| 			goto errEnvFile | 			goto errEnvFile | ||||||
| 		} | 		} | ||||||
| 		for key, val := range envMap { | 		for key, val := range envMap { | ||||||
| 			process.Setenv(key, GetVaultKey(val, opts, log)) | 			err = process.Setenv(key, getExternalConfigDirectiveValue(val, opts, AllowedExternalDirectiveVault)) | ||||||
|  | 			if err != nil { | ||||||
|  | 				log.Error().Err(err).Send() | ||||||
|  |  | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -119,12 +132,15 @@ errEnvFile: | |||||||
| 		if strings.Contains(envVal, "=") { | 		if strings.Contains(envVal, "=") { | ||||||
| 			envVarArr := strings.Split(envVal, "=") | 			envVarArr := strings.Split(envVal, "=") | ||||||
|  |  | ||||||
| 			process.Setenv(envVarArr[0], GetVaultKey(envVarArr[1], opts, log)) | 			err := process.Setenv(envVarArr[0], getExternalConfigDirectiveValue(envVarArr[1], opts, AllowedExternalDirectiveVaultFile)) | ||||||
|  | 			if err != nil { | ||||||
|  | 				log.Error().Err(err).Send() | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func injectEnvIntoLocalCMD(envVarsToInject environmentVars, process *exec.Cmd, log zerolog.Logger) { | func injectEnvIntoLocalCMD(envVarsToInject environmentVars, process *exec.Cmd, log zerolog.Logger, opts *ConfigOpts) { | ||||||
| 	if envVarsToInject.file != "" { | 	if envVarsToInject.file != "" { | ||||||
| 		envPath, _ := getFullPathWithHomeDir(envVarsToInject.file) | 		envPath, _ := getFullPathWithHomeDir(envVarsToInject.file) | ||||||
|  |  | ||||||
| @@ -148,7 +164,8 @@ errEnvFile: | |||||||
|  |  | ||||||
| 	for _, envVal := range envVarsToInject.env { | 	for _, envVal := range envVarsToInject.env { | ||||||
| 		if strings.Contains(envVal, "=") { | 		if strings.Contains(envVal, "=") { | ||||||
| 			process.Env = append(process.Env, envVal) | 			envVarArr := strings.Split(envVal, "=") | ||||||
|  | 			process.Env = append(process.Env, fmt.Sprintf("%s=%s", envVarArr[0], getExternalConfigDirectiveValue(envVarArr[1], opts, AllowedExternalDirectiveVault))) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	process.Env = append(process.Env, os.Environ()...) | 	process.Env = append(process.Env, os.Environ()...) | ||||||
| @@ -181,7 +198,6 @@ func testFile(c string) error { | |||||||
| 			return fileOpenErr | 			return fileOpenErr | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -247,11 +263,9 @@ func (opts *ConfigOpts) loadEnv() { | |||||||
| 	opts.backyEnv = backyEnv | 	opts.backyEnv = backyEnv | ||||||
| } | } | ||||||
|  |  | ||||||
| // expandEnvVars expands environment variables with the env used in the config |  | ||||||
| func expandEnvVars(backyEnv map[string]string, envVars []string) { | func expandEnvVars(backyEnv map[string]string, envVars []string) { | ||||||
|  |  | ||||||
| 	env := func(name string) string { | 	env := func(name string) string { | ||||||
| 		name = strings.ToUpper(name) |  | ||||||
| 		envVar, found := backyEnv[name] | 		envVar, found := backyEnv[name] | ||||||
| 		if found { | 		if found { | ||||||
| 			return envVar | 			return envVar | ||||||
| @@ -259,36 +273,35 @@ func expandEnvVars(backyEnv map[string]string, envVars []string) { | |||||||
| 		return "" | 		return "" | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// parse env variables using new macros |  | ||||||
| 	for indx, v := range envVars { | 	for indx, v := range envVars { | ||||||
| 		if strings.HasPrefix(v, macroStart) && strings.HasSuffix(v, macroEnd) { |  | ||||||
| 			if strings.HasPrefix(v, envMacroStart) { | 		if strings.HasPrefix(v, envExternDirectiveStart) && strings.HasSuffix(v, externDirectiveEnd) { | ||||||
| 				v = strings.TrimPrefix(v, envMacroStart) | 			v = strings.TrimPrefix(v, envExternDirectiveStart) | ||||||
| 				v = strings.TrimRight(v, macroEnd) | 			v = strings.TrimRight(v, externDirectiveEnd) | ||||||
| 			out, _ := shell.Expand(v, env) | 			out, _ := shell.Expand(v, env) | ||||||
| 			envVars[indx] = out | 			envVars[indx] = out | ||||||
| 		} | 		} | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func getCommandTypeAndSetCommandInfo(command *Command) *Command { | func getCommandTypeAndSetCommandInfo(command *Command) *Command { | ||||||
|  |  | ||||||
| 	if command.Type == PackageCT && !command.packageCmdSet { | 	if command.Type == PackageCommandType && !command.packageCmdSet { | ||||||
| 		command.packageCmdSet = true | 		command.packageCmdSet = true | ||||||
| 		switch command.PackageOperation { | 		switch command.PackageOperation { | ||||||
| 		case PackOpInstall: | 		case PackageOperationInstall: | ||||||
| 			command.Cmd, command.Args = command.pkgMan.Install(command.PackageName, command.PackageVersion, command.Args) | 			command.Cmd, command.Args = command.pkgMan.Install(command.Packages, command.Args) | ||||||
| 		case PackOpRemove: | 		case PackageOperationRemove: | ||||||
| 			command.Cmd, command.Args = command.pkgMan.Remove(command.PackageName, command.Args) | 			command.Cmd, command.Args = command.pkgMan.Remove(command.Packages, command.Args) | ||||||
| 		case PackOpUpgrade: | 		case PackageOperationUpgrade: | ||||||
| 			command.Cmd, command.Args = command.pkgMan.Upgrade(command.PackageName, command.PackageVersion) | 			command.Cmd, command.Args = command.pkgMan.Upgrade(command.Packages) | ||||||
| 		case PackOpCheckVersion: | 		case PackageOperationCheckVersion: | ||||||
| 			command.Cmd, command.Args = command.pkgMan.CheckVersion(command.PackageName, command.PackageVersion) | 			command.Cmd, command.Args = command.pkgMan.CheckVersion(command.Packages) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if command.Type == UserCT && !command.userCmdSet { | 	if command.Type == UserCommandType && !command.userCmdSet { | ||||||
| 		command.userCmdSet = true | 		command.userCmdSet = true | ||||||
| 		switch command.UserOperation { | 		switch command.UserOperation { | ||||||
| 		case "add": | 		case "add": | ||||||
| @@ -296,7 +309,8 @@ func getCommandTypeAndSetCommandInfo(command *Command) *Command { | |||||||
| 				command.Username, | 				command.Username, | ||||||
| 				command.UserHome, | 				command.UserHome, | ||||||
| 				command.UserShell, | 				command.UserShell, | ||||||
| 				command.SystemUser, | 				command.UserIsSystem, | ||||||
|  | 				command.UserCreateHome, | ||||||
| 				command.UserGroups, | 				command.UserGroups, | ||||||
| 				command.Args) | 				command.Args) | ||||||
| 		case "modify": | 		case "modify": | ||||||
| @@ -320,32 +334,188 @@ func getCommandTypeAndSetCommandInfo(command *Command) *Command { | |||||||
| func parsePackageVersion(output string, cmdCtxLogger zerolog.Logger, command *Command, cmdOutBuf bytes.Buffer) ([]string, error) { | func parsePackageVersion(output string, cmdCtxLogger zerolog.Logger, command *Command, cmdOutBuf bytes.Buffer) ([]string, error) { | ||||||
|  |  | ||||||
| 	var err error | 	var err error | ||||||
| 	pkgVersion, err := command.pkgMan.Parse(output) | 	var errs []error | ||||||
| 	// println(output) | 	pkgVersionOnSystem, err := command.pkgMan.ParseRemotePackageManagerVersionOutput(output) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		cmdCtxLogger.Error().Err(err).Str("package", command.PackageName).Msg("Error parsing package version output") | 		cmdCtxLogger.Error().AnErr("Error parsing package version output", err).Send() | ||||||
| 		return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.GetOutput), err | 		return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error parsing package version output: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	for _, p := range pkgVersionOnSystem { | ||||||
|  | 		packageIndex := getPackageIndexFromCommand(command, p.Name) | ||||||
|  | 		if packageIndex == -1 { | ||||||
|  | 			cmdCtxLogger.Error().Str("package", p.Name).Msg("Package not found in command") | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		command.Packages[packageIndex].VersionCheck = p.VersionCheck | ||||||
|  | 		packageFromCommand := command.Packages[packageIndex] | ||||||
| 		cmdCtxLogger.Info(). | 		cmdCtxLogger.Info(). | ||||||
| 		Str("Installed", pkgVersion.Installed). | 			Str("Installed", packageFromCommand.VersionCheck.Installed). | ||||||
| 		Str("Candidate", pkgVersion.Candidate). |  | ||||||
| 			Msg("Package version comparison") | 			Msg("Package version comparison") | ||||||
|  |  | ||||||
| 	if command.PackageVersion != "" { | 		versionLogger := cmdCtxLogger.With().Str("package", packageFromCommand.Name).Logger() | ||||||
| 		if pkgVersion.Installed == command.PackageVersion { |  | ||||||
| 			cmdCtxLogger.Info().Msgf("Installed version matches specified version: %s", command.PackageVersion) | 		if packageFromCommand.Version != "" { | ||||||
|  | 			versionLogger := cmdCtxLogger.With().Str("package", packageFromCommand.Name).Str("Specified Version", packageFromCommand.Version).Logger() | ||||||
|  | 			packageVersionRegex, PkgRegexErr := regexp.Compile(packageFromCommand.Version) | ||||||
|  | 			if PkgRegexErr != nil { | ||||||
|  | 				versionLogger.Error().Err(PkgRegexErr).Msg("Error compiling package version regex") | ||||||
|  | 				errs = append(errs, PkgRegexErr) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			if p.Version == packageFromCommand.Version { | ||||||
|  | 				versionLogger.Info().Msgf("Installed version matches specified version: %s", packageFromCommand.Version) | ||||||
|  | 			} else if packageVersionRegex.MatchString(p.VersionCheck.Installed) { | ||||||
|  | 				versionLogger.Info().Msgf("Installed version contains specified version: %s", packageFromCommand.Version) | ||||||
| 			} else { | 			} else { | ||||||
| 			cmdCtxLogger.Info().Msgf("Installed version does not match specified version: %s", command.PackageVersion) | 				versionLogger.Info().Msg("Installed version does not match specified version") | ||||||
| 			err = fmt.Errorf("Installed version does not match specified version: %s", command.PackageVersion) | 				errs = append(errs, fmt.Errorf("installed version of %s does not match specified version: %s", packageFromCommand.Name, packageFromCommand.Version)) | ||||||
| 			} | 			} | ||||||
| 		} else { | 		} else { | ||||||
| 		if pkgVersion.Installed == pkgVersion.Candidate { | 			if p.VersionCheck.Installed == p.VersionCheck.Candidate { | ||||||
| 			cmdCtxLogger.Info().Msg("Installed and Candidate versions match") | 				versionLogger.Info().Msg("Installed and Candidate versions match") | ||||||
| 			} else { | 			} else { | ||||||
| 				cmdCtxLogger.Info().Msg("Installed and Candidate versions differ") | 				cmdCtxLogger.Info().Msg("Installed and Candidate versions differ") | ||||||
| 			err = errors.New("Installed and Candidate versions differ") | 				errs = append(errs, errors.New("installed and Candidate versions differ")) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, false), err | 	} | ||||||
|  | 	if errs == nil { | ||||||
|  | 		return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), nil | ||||||
|  | 	} | ||||||
|  | 	return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error parsing package version output: %v", errs) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getPackageIndexFromCommand(command *Command, name string) int { | ||||||
|  | 	for i, v := range command.Packages { | ||||||
|  | 		if name == v.Name { | ||||||
|  | 			return i | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return -1 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getExternalConfigDirectiveValue(key string, opts *ConfigOpts, allowedDirectives AllowedExternalDirectives) string { | ||||||
|  | 	if !(strings.HasPrefix(key, externDirectiveStart) && strings.HasSuffix(key, externDirectiveEnd)) { | ||||||
|  | 		return key | ||||||
|  | 	} | ||||||
|  | 	key = replaceVarInString(opts.Vars, key, opts.Logger) | ||||||
|  | 	opts.Logger.Debug().Str("expanding external key", key).Send() | ||||||
|  |  | ||||||
|  | 	if strings.HasPrefix(key, envExternDirectiveStart) { | ||||||
|  | 		if IsExternalDirectiveEnv(allowedDirectives) { | ||||||
|  |  | ||||||
|  | 			key = strings.TrimPrefix(key, envExternDirectiveStart) | ||||||
|  | 			key = strings.TrimSuffix(key, externDirectiveEnd) | ||||||
|  | 			key = os.Getenv(key) | ||||||
|  | 		} else { | ||||||
|  | 			opts.Logger.Error().Msgf("Config key with value %s does not support env directive", key) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if strings.HasPrefix(key, externFileDirectiveStart) { | ||||||
|  | 		if IsExternalDirectiveFile(allowedDirectives) { | ||||||
|  |  | ||||||
|  | 			var err error | ||||||
|  | 			var keyValue []byte | ||||||
|  | 			key = strings.TrimPrefix(key, externFileDirectiveStart) | ||||||
|  | 			key = strings.TrimSuffix(key, externDirectiveEnd) | ||||||
|  | 			key, err = getFullPathWithHomeDir(key) | ||||||
|  | 			if err != nil { | ||||||
|  | 				opts.Logger.Err(err).Send() | ||||||
|  | 				return "" | ||||||
|  | 			} | ||||||
|  | 			if !path.IsAbs(key) { | ||||||
|  | 				key = path.Join(opts.ConfigDir, key) | ||||||
|  | 			} | ||||||
|  | 			keyValue, err = os.ReadFile(key) | ||||||
|  | 			if err != nil { | ||||||
|  | 				opts.Logger.Err(err).Send() | ||||||
|  | 				return "" | ||||||
|  | 			} | ||||||
|  | 			key = string(keyValue) | ||||||
|  | 		} else { | ||||||
|  | 			opts.Logger.Error().Msgf("Config key with value %s does not support file directive", key) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if strings.HasPrefix(key, vaultExternDirectiveStart) { | ||||||
|  | 		if IsExternalDirectiveVault(allowedDirectives) { | ||||||
|  |  | ||||||
|  | 			key = strings.TrimPrefix(key, vaultExternDirectiveStart) | ||||||
|  | 			key = strings.TrimSuffix(key, externDirectiveEnd) | ||||||
|  | 			key = GetVaultKey(key, opts, opts.Logger) | ||||||
|  | 		} else { | ||||||
|  | 			opts.Logger.Error().Msgf("Config key with value %s does not support vault directive", key) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return key | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getVaultSecret(vaultClient *vault.Client, key *VaultKey) (string, error) { | ||||||
|  | 	var ( | ||||||
|  | 		secret *vault.KVSecret | ||||||
|  | 		err    error | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	if key.ValueType == "KVv2" { | ||||||
|  | 		secret, err = vaultClient.KVv2(key.MountPath).Get(context.Background(), key.Path) | ||||||
|  | 	} else if key.ValueType == "KVv1" { | ||||||
|  | 		secret, err = vaultClient.KVv1(key.MountPath).Get(context.Background(), key.Path) | ||||||
|  | 	} else if key.ValueType != "" { | ||||||
|  | 		return "", fmt.Errorf("type %s for key %s not known. Valid types are KVv1 or KVv2", key.ValueType, key.Name) | ||||||
|  | 	} else { | ||||||
|  | 		return "", fmt.Errorf("type for key %s must be specified. Valid types are KVv1 or KVv2", key.Name) | ||||||
|  | 	} | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", fmt.Errorf("unable to read secret: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	value, ok := secret.Data[key.Key].(string) | ||||||
|  | 	if !ok { | ||||||
|  | 		return "", fmt.Errorf("value type assertion failed for vault key %s: %T %#v", key.Name, secret.Data[key.Name], secret.Data[key.Name]) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return value, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getVaultKeyData(keyName string, keys []*VaultKey) (*VaultKey, error) { | ||||||
|  | 	for _, k := range keys { | ||||||
|  | 		if k.Name == keyName { | ||||||
|  | 			return k, nil | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return nil, fmt.Errorf("key %s not found in vault keys", keyName) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func GetVaultKey(str string, opts *ConfigOpts, log zerolog.Logger) string { | ||||||
|  | 	key, err := getVaultKeyData(str, opts.VaultKeys) | ||||||
|  | 	if key == nil && err == nil { | ||||||
|  | 		return str | ||||||
|  | 	} | ||||||
|  | 	if err != nil && key == nil { | ||||||
|  | 		log.Err(err).Send() | ||||||
|  | 		return "" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	value, secretErr := getVaultSecret(opts.vaultClient, key) | ||||||
|  | 	if secretErr != nil { | ||||||
|  | 		log.Err(secretErr).Send() | ||||||
|  | 		return value | ||||||
|  | 	} | ||||||
|  | 	return value | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func IsExternalDirectiveFile(allowedExternalDirectives AllowedExternalDirectives) bool { | ||||||
|  | 	return strings.Contains(allowedExternalDirectives.String(), "file") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func IsExternalDirectiveEnv(allowedExternalDirectives AllowedExternalDirectives) bool { | ||||||
|  | 	return strings.Contains(allowedExternalDirectives.String(), "env") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func IsExternalDirectiveVault(allowedExternalDirectives AllowedExternalDirectives) bool { | ||||||
|  | 	return strings.Contains(allowedExternalDirectives.String(), "vault") | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,18 +1,19 @@ | |||||||
| package apt | package apt | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"bufio" | ||||||
|  | 	"bytes" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"regexp" |  | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/pkgcommon" | 	packagemanagercommon "git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/common" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // AptManager implements PackageManager for systems using APT. | // AptManager implements PackageManager for systems using APT. | ||||||
| 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 | 	Parser      packagemanagercommon.PackageParser | ||||||
| } | } | ||||||
|  |  | ||||||
| // DefaultAuthCommand is the default command used for authentication. | // DefaultAuthCommand is the default command used for authentication. | ||||||
| @@ -29,14 +30,13 @@ func NewAptManager() *AptManager { | |||||||
| } | } | ||||||
|  |  | ||||||
| // Install returns the command and arguments for installing a package. | // Install returns the command and arguments for installing a package. | ||||||
| func (a *AptManager) Install(pkg, version string, args []string) (string, []string) { | func (a *AptManager) Install(pkgs []packagemanagercommon.Package, args []string) (string, []string) { | ||||||
| 	baseCmd := a.prependAuthCommand(DefaultPackageCommand) | 	baseCmd := a.prependAuthCommand(DefaultPackageCommand) | ||||||
| 	baseArgs := []string{"update", "&&", baseCmd, "install", "-y"} | 	baseArgs := []string{"update", "&&", baseCmd, "install", "-y"} | ||||||
| 	if version != "" { | 	for _, p := range pkgs { | ||||||
| 		baseArgs = append(baseArgs, fmt.Sprintf("%s=%s", pkg, version)) | 		baseArgs = append(baseArgs, p.Name) | ||||||
| 	} else { |  | ||||||
| 		baseArgs = append(baseArgs, pkg) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if args != nil { | 	if args != nil { | ||||||
| 		baseArgs = append(baseArgs, args...) | 		baseArgs = append(baseArgs, args...) | ||||||
| 	} | 	} | ||||||
| @@ -44,31 +44,34 @@ func (a *AptManager) Install(pkg, version string, args []string) (string, []stri | |||||||
| } | } | ||||||
|  |  | ||||||
| // Remove returns the command and arguments for removing a package. | // Remove returns the command and arguments for removing a package. | ||||||
| func (a *AptManager) Remove(pkg string, args []string) (string, []string) { | func (a *AptManager) Remove(pkgs []packagemanagercommon.Package, args []string) (string, []string) { | ||||||
| 	baseCmd := a.prependAuthCommand(DefaultPackageCommand) | 	baseCmd := a.prependAuthCommand(DefaultPackageCommand) | ||||||
| 	baseArgs := []string{"remove", "-y", pkg} | 	baseArgs := []string{"remove", "-y"} | ||||||
|  | 	for _, p := range pkgs { | ||||||
|  | 		baseArgs = append(baseArgs, p.Name) | ||||||
|  | 	} | ||||||
| 	if args != nil { | 	if args != nil { | ||||||
| 		baseArgs = append(baseArgs, args...) | 		baseArgs = append(baseArgs, args...) | ||||||
| 	} | 	} | ||||||
| 	return baseCmd, baseArgs | 	return baseCmd, baseArgs | ||||||
| } | } | ||||||
|  |  | ||||||
| // Upgrade returns the command and arguments for upgrading a specific package. | func (a *AptManager) Upgrade(pkgs []packagemanagercommon.Package) (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 != "" { | 	for _, p := range pkgs { | ||||||
| 		baseArgs = append(baseArgs, fmt.Sprintf("%s=%s", pkg, version)) | 		baseArgs = append(baseArgs, p.Name) | ||||||
| 	} else { |  | ||||||
| 		baseArgs = append(baseArgs, pkg) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return baseCmd, baseArgs | 	return baseCmd, baseArgs | ||||||
| } | } | ||||||
|  |  | ||||||
| // CheckVersion returns the command and arguments for checking the info of a specific package. | func (a *AptManager) CheckVersion(pkgs []packagemanagercommon.Package) (string, []string) { | ||||||
| func (a *AptManager) CheckVersion(pkg, version string) (string, []string) { |  | ||||||
| 	baseCmd := a.prependAuthCommand("apt-cache") | 	baseCmd := a.prependAuthCommand("apt-cache") | ||||||
| 	baseArgs := []string{"policy", pkg} | 	baseArgs := []string{"policy"} | ||||||
|  | 	for _, p := range pkgs { | ||||||
|  | 		baseArgs = append(baseArgs, p.Name) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return baseCmd, baseArgs | 	return baseCmd, baseArgs | ||||||
| } | } | ||||||
| @@ -81,7 +84,7 @@ func (a *AptManager) UpgradeAll() (string, []string) { | |||||||
| } | } | ||||||
|  |  | ||||||
| // Configure applies functional options to customize the package manager. | // Configure applies functional options to customize the package manager. | ||||||
| func (a *AptManager) Configure(options ...pkgcommon.PackageManagerOption) { | func (a *AptManager) Configure(options ...packagemanagercommon.PackageManagerOption) { | ||||||
| 	for _, opt := range options { | 	for _, opt := range options { | ||||||
| 		opt(a) | 		opt(a) | ||||||
| 	} | 	} | ||||||
| @@ -106,25 +109,64 @@ func (a *AptManager) SetAuthCommand(authCommand string) { | |||||||
| } | } | ||||||
|  |  | ||||||
| // Parse parses the apt-cache policy output to extract Installed and Candidate versions. | // Parse parses the apt-cache policy output to extract Installed and Candidate versions. | ||||||
| func (a *AptManager) Parse(output string) (*pkgcommon.PackageVersion, error) { | func (a *AptManager) ParseRemotePackageManagerVersionOutput(output string) ([]packagemanagercommon.Package, error) { | ||||||
|  | 	var ( | ||||||
|  | 		packageName        string | ||||||
|  | 		installedString    string | ||||||
|  | 		candidateString    string | ||||||
|  | 		countRelevantLines int | ||||||
|  | 	) | ||||||
| 	// Check for error message in the output | 	// Check for error message in the output | ||||||
| 	if strings.Contains(output, "Unable to locate package") { | 	if strings.Contains(output, "Unable to locate package") { | ||||||
| 		return nil, fmt.Errorf("error: %s", strings.TrimSpace(output)) | 		return nil, fmt.Errorf("error: %s", strings.TrimSpace(output)) | ||||||
| 	} | 	} | ||||||
|  | 	packages := []packagemanagercommon.Package{} | ||||||
| 	reInstalled := regexp.MustCompile(`Installed:\s*([^\s]+)`) | 	outputBuf := bytes.NewBufferString(output) | ||||||
| 	reCandidate := regexp.MustCompile(`Candidate:\s*([^\s]+)`) | 	outputScan := bufio.NewScanner(outputBuf) | ||||||
|  | 	var packageCount uint | ||||||
| 	installedMatch := reInstalled.FindStringSubmatch(output) | 	for outputScan.Scan() { | ||||||
| 	candidateMatch := reCandidate.FindStringSubmatch(output) | 		line := outputScan.Text() | ||||||
|  | 		if !strings.HasPrefix(line, "  ") && strings.HasSuffix(line, ":") { | ||||||
| 	if len(installedMatch) < 2 || len(candidateMatch) < 2 { | 			// count++ | ||||||
| 		return nil, fmt.Errorf("failed to parse Installed or Candidate versions from apt output. check package name") | 			packageName = strings.TrimSpace(strings.TrimSuffix(line, ":")) | ||||||
|  | 		} | ||||||
|  | 		if strings.Contains(line, "Installed:") { | ||||||
|  | 			countRelevantLines++ | ||||||
|  | 			installedString = strings.TrimPrefix(strings.TrimSpace(line), "Installed:") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 	return &pkgcommon.PackageVersion{ | 		if strings.Contains(line, "Candidate:") { | ||||||
| 		Installed: strings.TrimSpace(installedMatch[1]), | 			countRelevantLines++ | ||||||
| 		Candidate: strings.TrimSpace(candidateMatch[1]), | 			candidateString = strings.TrimPrefix(strings.TrimSpace(line), "Candidate:") | ||||||
| 		Match:     installedMatch[1] == candidateMatch[1], | 		} | ||||||
| 	}, nil |  | ||||||
|  | 		if countRelevantLines == 2 { | ||||||
|  | 			countRelevantLines = 0 | ||||||
|  | 			if !strings.Contains(installedString, " (none)") { | ||||||
|  | 				packageCount++ | ||||||
|  | 				packages = append(packages, packagemanagercommon.Package{ | ||||||
|  | 					Name: packageName, | ||||||
|  | 					VersionCheck: packagemanagercommon.PackageVersion{ | ||||||
|  | 						Installed: strings.TrimSpace(installedString), | ||||||
|  | 						Candidate: strings.TrimSpace(candidateString), | ||||||
|  | 						Match:     installedString == candidateString, | ||||||
|  | 					}}, | ||||||
|  | 				) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if packageCount == 0 { | ||||||
|  | 		return nil, fmt.Errorf("no packages found") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return packages, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func SearchPackages(pkgs []string, version string) (string, []string) { | ||||||
|  | 	baseCommand := "dpkg-query" | ||||||
|  | 	baseArgs := []string{"-W", "-f='${Package}\t${Architecture}\t${db:Status-Status}\t${Version}\t${Installed-Size}\t${Binary:summary}\n'"} | ||||||
|  | 	baseArgs = append(baseArgs, pkgs...) | ||||||
|  |  | ||||||
|  | 	return baseCommand, baseArgs | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| package pkgcommon | package packagemanagercommon | ||||||
| 
 | 
 | ||||||
| // 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{}) | ||||||
| @@ -15,3 +15,9 @@ type PackageVersion struct { | |||||||
| 	Match     bool | 	Match     bool | ||||||
| 	Message   string | 	Message   string | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | type Package struct { | ||||||
|  | 	Name         string `yaml:"name"` | ||||||
|  | 	Version      string `yaml:"version,omitempty"` | ||||||
|  | 	VersionCheck PackageVersion | ||||||
|  | } | ||||||
| @@ -5,7 +5,7 @@ import ( | |||||||
| 	"regexp" | 	"regexp" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/pkgcommon" | 	packagemanagercommon "git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/common" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // DnfManager implements PackageManager for systems using YUM. | // DnfManager implements PackageManager for systems using YUM. | ||||||
| @@ -26,21 +26,21 @@ func NewDnfManager() *DnfManager { | |||||||
| } | } | ||||||
|  |  | ||||||
| // Configure applies functional options to customize the package manager. | // Configure applies functional options to customize the package manager. | ||||||
| func (y *DnfManager) Configure(options ...pkgcommon.PackageManagerOption) { | func (y *DnfManager) Configure(options ...packagemanagercommon.PackageManagerOption) { | ||||||
| 	for _, opt := range options { | 	for _, opt := range options { | ||||||
| 		opt(y) | 		opt(y) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // Install returns the command and arguments for installing a package. | // Install returns the command and arguments for installing a package. | ||||||
| func (y *DnfManager) Install(pkg, version string, args []string) (string, []string) { | func (y *DnfManager) Install(pkgs []packagemanagercommon.Package, args []string) (string, []string) { | ||||||
| 	baseCmd := y.prependAuthCommand("dnf") | 	baseCmd := y.prependAuthCommand("dnf") | ||||||
| 	baseArgs := []string{"install", "-y"} | 	baseArgs := []string{"install", "-y"} | ||||||
| 	if version != "" { |  | ||||||
| 		baseArgs = append(baseArgs, fmt.Sprintf("%s-%s", pkg, version)) | 	for _, p := range pkgs { | ||||||
| 	} else { | 		baseArgs = append(baseArgs, p.Name) | ||||||
| 		baseArgs = append(baseArgs, pkg) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if args != nil { | 	if args != nil { | ||||||
| 		baseArgs = append(baseArgs, args...) | 		baseArgs = append(baseArgs, args...) | ||||||
| 	} | 	} | ||||||
| @@ -48,9 +48,13 @@ func (y *DnfManager) Install(pkg, version string, args []string) (string, []stri | |||||||
| } | } | ||||||
|  |  | ||||||
| // Remove returns the command and arguments for removing a package. | // Remove returns the command and arguments for removing a package. | ||||||
| func (y *DnfManager) Remove(pkg string, args []string) (string, []string) { | func (y *DnfManager) Remove(pkgs []packagemanagercommon.Package, args []string) (string, []string) { | ||||||
| 	baseCmd := y.prependAuthCommand("dnf") | 	baseCmd := y.prependAuthCommand("dnf") | ||||||
| 	baseArgs := []string{"remove", "-y", pkg} | 	baseArgs := []string{"remove", "-y"} | ||||||
|  | 	for _, p := range pkgs { | ||||||
|  | 		baseArgs = append(baseArgs, p.Name) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if args != nil { | 	if args != nil { | ||||||
| 		baseArgs = append(baseArgs, args...) | 		baseArgs = append(baseArgs, args...) | ||||||
| 	} | 	} | ||||||
| @@ -58,34 +62,37 @@ func (y *DnfManager) 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 (y *DnfManager) Upgrade(pkg, version string) (string, []string) { | func (y *DnfManager) Upgrade(pkgs []packagemanagercommon.Package) (string, []string) { | ||||||
| 	baseCmd := y.prependAuthCommand("dnf") | 	baseCmd := y.prependAuthCommand("dnf") | ||||||
| 	baseArgs := []string{"update", "-y"} | 	baseArgs := []string{"update", "-y"} | ||||||
| 	if version != "" { |  | ||||||
| 		baseArgs = append(baseArgs, fmt.Sprintf("%s-%s", pkg, version)) | 	for _, p := range pkgs { | ||||||
| 	} else { | 		baseArgs = append(baseArgs, p.Name) | ||||||
| 		baseArgs = append(baseArgs, pkg) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return baseCmd, baseArgs | 	return baseCmd, baseArgs | ||||||
| } | } | ||||||
|  |  | ||||||
| // UpgradeAll returns the command and arguments for upgrading all packages. | // UpgradeAll returns the command and arguments for upgrading all packages. | ||||||
| func (y *DnfManager) UpgradeAll() (string, []string) { | func (y *DnfManager) UpgradeAll() (string, []string) { | ||||||
| 	baseCmd := y.prependAuthCommand("dnf") | 	baseCmd := y.prependAuthCommand("dnf") | ||||||
| 	baseArgs := []string{"update", "-y"} | 	baseArgs := []string{"upgrade", "-y"} | ||||||
| 	return baseCmd, baseArgs | 	return baseCmd, baseArgs | ||||||
| } | } | ||||||
|  |  | ||||||
| // CheckVersion returns the command and arguments for checking the info of a specific package. | // CheckVersion returns the command and arguments for checking the info of a specific package. | ||||||
| func (d *DnfManager) CheckVersion(pkg, version string) (string, []string) { | func (d *DnfManager) CheckVersion(pkgs []packagemanagercommon.Package) (string, []string) { | ||||||
| 	baseCmd := d.prependAuthCommand("dnf") | 	baseCmd := d.prependAuthCommand("dnf") | ||||||
| 	baseArgs := []string{"info", pkg} | 	baseArgs := []string{"info"} | ||||||
|  | 	for _, p := range pkgs { | ||||||
|  | 		baseArgs = append(baseArgs, p.Name) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return baseCmd, baseArgs | 	return baseCmd, baseArgs | ||||||
| } | } | ||||||
|  |  | ||||||
| // Parse parses the dnf info output to extract Installed and Candidate versions. | // Parse parses the dnf info output to extract Installed and Candidate versions. | ||||||
| func (d DnfManager) Parse(output string) (*pkgcommon.PackageVersion, error) { | func (d DnfManager) ParseRemotePackageManagerVersionOutput(output string) ([]packagemanagercommon.Package, error) { | ||||||
|  |  | ||||||
| 	// Check for error message in the output | 	// Check for error message in the output | ||||||
| 	if strings.Contains(output, "No matching packages to list") { | 	if strings.Contains(output, "No matching packages to list") { | ||||||
| @@ -114,10 +121,7 @@ func (d DnfManager) Parse(output string) (*pkgcommon.PackageVersion, error) { | |||||||
| 		return nil, fmt.Errorf("failed to parse versions from dnf output") | 		return nil, fmt.Errorf("failed to parse versions from dnf output") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return &pkgcommon.PackageVersion{ | 	return nil, nil | ||||||
| 		Installed: installedVersion, |  | ||||||
| 		Candidate: candidateVersion, |  | ||||||
| 	}, nil |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // prependAuthCommand prepends the authentication command if UseAuth is true. | // prependAuthCommand prepends the authentication command if UseAuth is true. | ||||||
|   | |||||||
| @@ -4,26 +4,26 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
|  |  | ||||||
| 	"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/apt" | 	"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/apt" | ||||||
|  | 	packagemanagercommon "git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/common" | ||||||
| 	"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/dnf" | 	"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/dnf" | ||||||
| 	"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/pkgcommon" |  | ||||||
| 	"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/yum" | 	"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/yum" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // PackageManager is an interface used to define common package commands. This shall be implemented by every package. | // PackageManager is an interface used to define common package commands. This shall be implemented by every package. | ||||||
| type PackageManager interface { | type PackageManager interface { | ||||||
| 	Install(pkg, version string, args []string) (string, []string) | 	Install(pkgs []packagemanagercommon.Package, args []string) (string, []string) | ||||||
| 	Remove(pkg string, args []string) (string, []string) | 	Remove(pkgs []packagemanagercommon.Package, args []string) (string, []string) | ||||||
| 	Upgrade(pkg, version string) (string, []string) // Upgrade a specific package | 	Upgrade(pkgs []packagemanagercommon.Package) (string, []string) // Upgrade a specific package | ||||||
| 	UpgradeAll() (string, []string) | 	UpgradeAll() (string, []string) | ||||||
| 	CheckVersion(pkg, version string) (string, []string) | 	CheckVersion(pkgs []packagemanagercommon.Package) (string, []string) | ||||||
| 	Parse(output string) (*pkgcommon.PackageVersion, error) | 	ParseRemotePackageManagerVersionOutput(output string) ([]packagemanagercommon.Package, 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 ...packagemanagercommon.PackageManagerOption) | ||||||
| } | } | ||||||
|  |  | ||||||
| // PackageManagerFactory returns the appropriate PackageManager based on the package tool. | // PackageManagerFactory returns the appropriate PackageManager based on the package tool. | ||||||
| // Takes variable number of options. | // Takes variable number of options. | ||||||
| func PackageManagerFactory(managerType string, options ...pkgcommon.PackageManagerOption) (PackageManager, error) { | func PackageManagerFactory(managerType string, options ...packagemanagercommon.PackageManagerOption) (PackageManager, error) { | ||||||
| 	var manager PackageManager | 	var manager PackageManager | ||||||
|  |  | ||||||
| 	switch managerType { | 	switch managerType { | ||||||
| @@ -43,7 +43,7 @@ func PackageManagerFactory(managerType string, options ...pkgcommon.PackageManag | |||||||
| } | } | ||||||
|  |  | ||||||
| // WithAuth enables authentication and sets the authentication command. | // WithAuth enables authentication and sets the authentication command. | ||||||
| func WithAuth(authCommand string) pkgcommon.PackageManagerOption { | func WithAuth(authCommand string) packagemanagercommon.PackageManagerOption { | ||||||
| 	return func(manager interface{}) { | 	return func(manager interface{}) { | ||||||
| 		if configurable, ok := manager.(interface { | 		if configurable, ok := manager.(interface { | ||||||
| 			SetUseAuth(bool) | 			SetUseAuth(bool) | ||||||
| @@ -56,7 +56,7 @@ func WithAuth(authCommand string) pkgcommon.PackageManagerOption { | |||||||
| } | } | ||||||
|  |  | ||||||
| // WithoutAuth disables authentication. | // WithoutAuth disables authentication. | ||||||
| func WithoutAuth() pkgcommon.PackageManagerOption { | func WithoutAuth() packagemanagercommon.PackageManagerOption { | ||||||
| 	return func(manager interface{}) { | 	return func(manager interface{}) { | ||||||
| 		if configurable, ok := manager.(interface { | 		if configurable, ok := manager.(interface { | ||||||
| 			SetUseAuth(bool) | 			SetUseAuth(bool) | ||||||
| @@ -68,8 +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 | 	packagemanagercommon.PackageParser | ||||||
| 	SetUseAuth(useAuth bool) | 	SetUseAuth(useAuth bool) | ||||||
| 	SetAuthCommand(authCommand string) | 	SetAuthCommand(authCommand string) | ||||||
| 	SetPackageParser(parser pkgcommon.PackageParser) | 	SetPackageParser(parser packagemanagercommon.PackageParser) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,8 +3,9 @@ package yum | |||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"regexp" | 	"regexp" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
| 	"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/pkgcommon" | 	packagemanagercommon "git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/common" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // YumManager implements PackageManager for systems using YUM. | // YumManager implements PackageManager for systems using YUM. | ||||||
| @@ -25,21 +26,20 @@ func NewYumManager() *YumManager { | |||||||
| } | } | ||||||
|  |  | ||||||
| // Configure applies functional options to customize the package manager. | // Configure applies functional options to customize the package manager. | ||||||
| func (y *YumManager) Configure(options ...pkgcommon.PackageManagerOption) { | func (y *YumManager) Configure(options ...packagemanagercommon.PackageManagerOption) { | ||||||
| 	for _, opt := range options { | 	for _, opt := range options { | ||||||
| 		opt(y) | 		opt(y) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // Install returns the command and arguments for installing a package. | // Install returns the command and arguments for installing a package. | ||||||
| func (y *YumManager) Install(pkg, version string, args []string) (string, []string) { | func (y *YumManager) Install(pkgs []packagemanagercommon.Package, args []string) (string, []string) { | ||||||
| 	baseCmd := y.prependAuthCommand("yum") | 	baseCmd := y.prependAuthCommand("yum") | ||||||
| 	baseArgs := []string{"install", "-y"} | 	baseArgs := []string{"install", "-y"} | ||||||
| 	if version != "" { | 	for _, p := range pkgs { | ||||||
| 		baseArgs = append(baseArgs, fmt.Sprintf("%s-%s", pkg, version)) | 		baseArgs = append(baseArgs, p.Name) | ||||||
| 	} else { |  | ||||||
| 		baseArgs = append(baseArgs, pkg) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if args != nil { | 	if args != nil { | ||||||
| 		baseArgs = append(baseArgs, args...) | 		baseArgs = append(baseArgs, args...) | ||||||
| 	} | 	} | ||||||
| @@ -47,9 +47,13 @@ func (y *YumManager) Install(pkg, version string, args []string) (string, []stri | |||||||
| } | } | ||||||
|  |  | ||||||
| // Remove returns the command and arguments for removing a package. | // Remove returns the command and arguments for removing a package. | ||||||
| func (y *YumManager) Remove(pkg string, args []string) (string, []string) { | func (y *YumManager) Remove(pkgs []packagemanagercommon.Package, args []string) (string, []string) { | ||||||
| 	baseCmd := y.prependAuthCommand("yum") | 	baseCmd := y.prependAuthCommand("yum") | ||||||
| 	baseArgs := []string{"remove", "-y", pkg} | 	baseArgs := []string{"remove", "-y"} | ||||||
|  | 	for _, p := range pkgs { | ||||||
|  | 		baseArgs = append(baseArgs, p.Name) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if args != nil { | 	if args != nil { | ||||||
| 		baseArgs = append(baseArgs, args...) | 		baseArgs = append(baseArgs, args...) | ||||||
| 	} | 	} | ||||||
| @@ -57,14 +61,13 @@ func (y *YumManager) 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 (y *YumManager) Upgrade(pkg, version string) (string, []string) { | func (y *YumManager) Upgrade(pkgs []packagemanagercommon.Package) (string, []string) { | ||||||
| 	baseCmd := y.prependAuthCommand("yum") | 	baseCmd := y.prependAuthCommand("yum") | ||||||
| 	baseArgs := []string{"update", "-y"} | 	baseArgs := []string{"update", "-y"} | ||||||
| 	if version != "" { | 	for _, p := range pkgs { | ||||||
| 		baseArgs = append(baseArgs, fmt.Sprintf("%s-%s", pkg, version)) | 		baseArgs = append(baseArgs, p.Name) | ||||||
| 	} else { |  | ||||||
| 		baseArgs = append(baseArgs, pkg) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return baseCmd, baseArgs | 	return baseCmd, baseArgs | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -76,17 +79,27 @@ func (y *YumManager) UpgradeAll() (string, []string) { | |||||||
| } | } | ||||||
|  |  | ||||||
| // CheckVersion returns the command and arguments for checking the info of a specific package. | // CheckVersion returns the command and arguments for checking the info of a specific package. | ||||||
| func (y *YumManager) CheckVersion(pkg, version string) (string, []string) { | func (y *YumManager) CheckVersion(pkgs []packagemanagercommon.Package) (string, []string) { | ||||||
| 	baseCmd := y.prependAuthCommand("yum") | 	baseCmd := y.prependAuthCommand("yum") | ||||||
| 	baseArgs := []string{"info", pkg} | 	baseArgs := []string{"info"} | ||||||
|  | 	for _, p := range pkgs { | ||||||
|  | 		baseArgs = append(baseArgs, p.Name) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return baseCmd, baseArgs | 	return baseCmd, baseArgs | ||||||
| } | } | ||||||
|  |  | ||||||
| // Parse parses the dnf info output to extract Installed and Candidate versions. | // Parse parses the dnf info output to extract Installed and Candidate versions. | ||||||
| func (y YumManager) Parse(output string) (*pkgcommon.PackageVersion, error) { | func (y YumManager) ParseRemotePackageManagerVersionOutput(output string) ([]packagemanagercommon.Package, 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]+)`) | 	// 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) | 	installedMatch := reInstalled.FindStringSubmatch(output) | ||||||
| 	candidateMatch := reAvailable.FindStringSubmatch(output) | 	candidateMatch := reAvailable.FindStringSubmatch(output) | ||||||
| @@ -106,10 +119,7 @@ func (y YumManager) Parse(output string) (*pkgcommon.PackageVersion, error) { | |||||||
| 		return nil, fmt.Errorf("failed to parse versions from dnf output") | 		return nil, fmt.Errorf("failed to parse versions from dnf output") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return &pkgcommon.PackageVersion{ | 	return nil, nil | ||||||
| 		Installed: installedVersion, |  | ||||||
| 		Candidate: candidateVersion, |  | ||||||
| 	}, nil |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // prependAuthCommand prepends the authentication command if UseAuth is true. | // prependAuthCommand prepends the authentication command if UseAuth is true. | ||||||
|   | |||||||
| @@ -116,7 +116,7 @@ func (c *Cache) Set(source, hash string, data []byte, dataType string) (CacheDat | |||||||
| 	path := filepath.Join(c.dir, fmt.Sprintf("%s-%s", fileName, sourceHash)) | 	path := filepath.Join(c.dir, fmt.Sprintf("%s-%s", fileName, sourceHash)) | ||||||
|  |  | ||||||
| 	if _, err := os.Stat(path); os.IsNotExist(err) { | 	if _, err := os.Stat(path); os.IsNotExist(err) { | ||||||
| 		os.MkdirAll(c.dir, 0700) | 		_ = os.MkdirAll(c.dir, 0700) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	err := os.WriteFile(path, data, 0644) | 	err := os.WriteFile(path, data, 0644) | ||||||
| @@ -171,7 +171,7 @@ func (cf *CachedFetcher) Hash(data []byte) string { | |||||||
| func LoadMetadataFromFile(filePath string) ([]*CacheData, error) { | func LoadMetadataFromFile(filePath string) ([]*CacheData, error) { | ||||||
| 	if _, err := os.Stat(filePath); os.IsNotExist(err) { | 	if _, err := os.Stat(filePath); os.IsNotExist(err) { | ||||||
| 		// Create the file if it does not exist | 		// Create the file if it does not exist | ||||||
| 		os.MkdirAll(path.Dir(filePath), 0700) | 		_ = os.MkdirAll(path.Dir(filePath), 0700) | ||||||
| 		emptyData := []byte("[]") | 		emptyData := []byte("[]") | ||||||
| 		err := os.WriteFile(filePath, emptyData, 0644) | 		err := os.WriteFile(filePath, emptyData, 0644) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ type RemoteFetcher interface { | |||||||
|  |  | ||||||
| 	// Parse decodes the raw data into a Go structure (e.g., Commands, CommandLists) | 	// Parse decodes the raw data into a Go structure (e.g., Commands, CommandLists) | ||||||
| 	// Takes the raw data as input and populates the target interface | 	// Takes the raw data as input and populates the target interface | ||||||
| 	Parse(data []byte, target interface{}) error | 	Parse(data []byte, target any) error | ||||||
|  |  | ||||||
| 	// Hash returns the hash of the configuration data | 	// Hash returns the hash of the configuration data | ||||||
| 	Hash(data []byte) string | 	Hash(data []byte) string | ||||||
| @@ -29,7 +29,7 @@ func NewRemoteFetcher(source string, cache *Cache, options ...FetcherOption) (Re | |||||||
| 		option(&config) | 		option(&config) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// If FileType is empty (i.e. WithFileType was not called), yaml is the default file type | 	// WithFileType was not called. yaml is the default file type | ||||||
| 	if strings.TrimSpace(config.FileType) == "" { | 	if strings.TrimSpace(config.FileType) == "" { | ||||||
| 		config.FileType = "yaml" | 		config.FileType = "yaml" | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ func (l *LocalFetcher) Fetch(source string) ([]byte, error) { | |||||||
| 		if l.config.IgnoreFileNotFound { | 		if l.config.IgnoreFileNotFound { | ||||||
| 			return nil, ErrIgnoreFileNotFound | 			return nil, ErrIgnoreFileNotFound | ||||||
| 		} | 		} | ||||||
| 		return nil, nil | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	file, err := os.Open(source) | 	file, err := os.Open(source) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ func (l LinuxUserManager) NewLinuxManager() *LinuxUserManager { | |||||||
| } | } | ||||||
|  |  | ||||||
| // AddUser adds a new user to the system. | // AddUser adds a new user to the system. | ||||||
| func (l LinuxUserManager) AddUser(username, homeDir, shell string, isSystem bool, groups, args []string) (string, []string) { | func (l LinuxUserManager) AddUser(username, homeDir, shell string, isSystem, createHome bool, groups, args []string) (string, []string) { | ||||||
| 	baseArgs := []string{} | 	baseArgs := []string{} | ||||||
|  |  | ||||||
| 	if isSystem { | 	if isSystem { | ||||||
| @@ -38,6 +38,10 @@ func (l LinuxUserManager) AddUser(username, homeDir, shell string, isSystem bool | |||||||
| 		baseArgs = append(baseArgs, args...) | 		baseArgs = append(baseArgs, args...) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if createHome { | ||||||
|  | 		baseArgs = append(baseArgs, "-m") | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	args = append(baseArgs, username) | 	args = append(baseArgs, username) | ||||||
|  |  | ||||||
| 	cmd := "useradd" | 	cmd := "useradd" | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ import ( | |||||||
| // UserManager defines the interface for user management operations. | // UserManager defines the interface for user management operations. | ||||||
| // All functions but one return a string for the command and any args. | // All functions but one return a string for the command and any args. | ||||||
| type UserManager interface { | type UserManager interface { | ||||||
| 	AddUser(username, homeDir, shell string, isSystem bool, groups, args []string) (string, []string) | 	AddUser(username, homeDir, shell string, createHome, isSystem bool, groups, args []string) (string, []string) | ||||||
| 	RemoveUser(username string) (string, []string) | 	RemoveUser(username string) (string, []string) | ||||||
| 	ModifyUser(username, homeDir, shell string, groups []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 | 	// Modify password uses chpasswd for Linux systems to build the command to change the password | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								release
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										1
									
								
								release
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,5 +1,6 @@ | |||||||
| #!/bin/bash | #!/bin/bash | ||||||
| set -eou pipefail | set -eou pipefail | ||||||
|  | go mod tidy | ||||||
| go generate ./... | go generate ./... | ||||||
| CURRENT_TAG="$(go run backy.go version -V)" | CURRENT_TAG="$(go run backy.go version -V)" | ||||||
| goreleaser -f .goreleaser/github.yml check | goreleaser -f .goreleaser/github.yml check | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								tests/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | private | ||||||
							
								
								
									
										31
									
								
								tests/ErrorHook.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								tests/ErrorHook.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | commands: | ||||||
|  |   echoTestFail: | ||||||
|  |     cmd: ech | ||||||
|  |     shell: bash | ||||||
|  |     Args:  | ||||||
|  |       - hello world | ||||||
|  |     hooks: | ||||||
|  |       error: | ||||||
|  |         - errorCmd | ||||||
|  |       final: | ||||||
|  |         - finalCmd | ||||||
|  |    | ||||||
|  |   finalCmd: | ||||||
|  |     cmd: echo | ||||||
|  |     Args: | ||||||
|  |       - "echo test fail" | ||||||
|  |  | ||||||
|  |   errorCmd: | ||||||
|  |     name: get docker version | ||||||
|  |     cmd: docker | ||||||
|  |     getOutput: true | ||||||
|  |     outputToLog: true | ||||||
|  |     Args: | ||||||
|  |       - "-v" | ||||||
|  |  | ||||||
|  | cmdLists: | ||||||
|  |   TestHooks: | ||||||
|  |     output: | ||||||
|  |       onFailure: true | ||||||
|  |     order: | ||||||
|  |       - echoTestFail | ||||||
							
								
								
									
										14
									
								
								tests/HookNotInFile.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								tests/HookNotInFile.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | commands: | ||||||
|  |   echoTestFail: | ||||||
|  |     cmd: ech | ||||||
|  |     shell: bash | ||||||
|  |     Args: hello world | ||||||
|  |     hooks: | ||||||
|  |       error: | ||||||
|  |         - errorCm #  | ||||||
|  |  | ||||||
|  |   errorCmd: | ||||||
|  |     name: get docker version | ||||||
|  |     cmd: docker | ||||||
|  |     Args: | ||||||
|  |       - "-v" | ||||||
							
								
								
									
										16
									
								
								tests/SuccessHook.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								tests/SuccessHook.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | commands: | ||||||
|  |   echoTestSuccess: | ||||||
|  |     cmd: echo | ||||||
|  |     shell: bash | ||||||
|  |     Args: hello world | ||||||
|  |     hooks: | ||||||
|  |       success: | ||||||
|  |         - successCmd | ||||||
|  |  | ||||||
|  |   errorCmd: | ||||||
|  |     name: get docker version | ||||||
|  |     cmd: docker | ||||||
|  |     getOutput: true | ||||||
|  |     outputToLog: true | ||||||
|  |     Args: | ||||||
|  |       - "-v" | ||||||
							
								
								
									
										27
									
								
								tests/VaultTest.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								tests/VaultTest.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | commands: | ||||||
|  |   vaultEnvVar: | ||||||
|  |     cmd: echo | ||||||
|  |     shell: /bin/zsh | ||||||
|  |     Args: | ||||||
|  |       - ${VAULT_VAR} | ||||||
|  |     environment: | ||||||
|  |       "VAULT_VAR=%{vault:vaultTestSecret}%" | ||||||
|  |  | ||||||
|  | logging: | ||||||
|  |   verbose: true | ||||||
|  |  | ||||||
|  | vault: | ||||||
|  |   token: root | ||||||
|  |   address: http://127.0.0.1:8200 | ||||||
|  |   enabled: true | ||||||
|  |   keys: | ||||||
|  |     - name: vaultTestSecret | ||||||
|  |       key: data | ||||||
|  |       mountpath: secret | ||||||
|  |       path: test/var | ||||||
|  |       type: KVv2 # KVv1 or KVv2 | ||||||
|  |  | ||||||
|  | cmdLists: | ||||||
|  |   addUsers: | ||||||
|  |     order: | ||||||
|  |       - vaultEnvVar | ||||||
| @@ -1,18 +0,0 @@ | |||||||
| commands: |  | ||||||
|   echoTestPass: |  | ||||||
|     cmd: echo |  | ||||||
|     shell: bash |  | ||||||
|     Args: hello world |  | ||||||
|  |  | ||||||
|   runRemoteShellScriptSuccess: |  | ||||||
|     cmd:  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|   packageCommandSuccess: |  | ||||||
|     packageName: docker-ce |  | ||||||
|     Args: |  | ||||||
|       - docker-ce-cli |  | ||||||
|     packageManager: apt |  | ||||||
|     packageOperation: install |  | ||||||
|  |  | ||||||
|    |  | ||||||
							
								
								
									
										26
									
								
								tests/docker/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								tests/docker/Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | FROM ubuntu:latest | ||||||
|  |  | ||||||
|  | # Install SSH server | ||||||
|  | RUN apt-get update && \ | ||||||
|  |     apt-get install -y openssh-server && \ | ||||||
|  |     apt-get clean | ||||||
|  |  | ||||||
|  | RUN sed -i 's/PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config | ||||||
|  |  | ||||||
|  | RUN sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config | ||||||
|  |  | ||||||
|  | RUN useradd -m -s /bin/bash backy | ||||||
|  | RUN echo "backy:backy" | chpasswd | ||||||
|  | RUN echo "root:test" | chpasswd | ||||||
|  | COPY --chown=backy:backy backytest.pub /home/backy/.ssh/authorized_keys | ||||||
|  | COPY --chown=root:root backytest.pub /root/.ssh/authorized_keys | ||||||
|  |  | ||||||
|  | EXPOSE 22 | ||||||
|  | RUN mkdir /var/run/sshd | ||||||
|  | RUN chmod 0755 /var/run/sshd | ||||||
|  |  | ||||||
|  | RUN apt-get update && apt-get install -y curl | ||||||
|  |  | ||||||
|  | # Start SSH service | ||||||
|  | CMD ["/usr/sbin/sshd", "-D"] | ||||||
|  | # ENTRYPOINT service ssh start && bash | ||||||
							
								
								
									
										7
									
								
								tests/docker/backytest
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								tests/docker/backytest
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | -----BEGIN OPENSSH PRIVATE KEY----- | ||||||
|  | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW | ||||||
|  | QyNTUxOQAAACBtBASN+HMx/EVUIs5ThK9Nlw0wPFVt2rXsiNZlDN+CswAAAKAfFc5AHxXO | ||||||
|  | QAAAAAtzc2gtZWQyNTUxOQAAACBtBASN+HMx/EVUIs5ThK9Nlw0wPFVt2rXsiNZlDN+Csw | ||||||
|  | AAAEAxs6uRkenVbXPrjgbIv/1THXL6dUdgr5KaCd7uBVm0PW0EBI34czH8RVQizlOEr02X | ||||||
|  | DTA8VW3ateyI1mUM34KzAAAAGU1lZGlhIHVzZXIgc3RvcmFnZSBzZXJ2ZXIBAgME | ||||||
|  | -----END OPENSSH PRIVATE KEY----- | ||||||
							
								
								
									
										1
									
								
								tests/docker/backytest.pub
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/docker/backytest.pub
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG0EBI34czH8RVQizlOEr02XDTA8VW3ateyI1mUM34Kz Backy test | ||||||
							
								
								
									
										4
									
								
								tests/docker/buildDocker.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								tests/docker/buildDocker.sh
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | cd ~/Projects/backy/tests/docker | ||||||
|  | docker container rm -f ssh_server_container | ||||||
|  | docker build -t ssh_server_image . | ||||||
|  | docker run -d -p 2222:22 --name ssh_server_container ssh_server_image | ||||||
							
								
								
									
										6
									
								
								tests/hosts.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								tests/hosts.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | hosts: | ||||||
|  |   docker: | ||||||
|  |     port: 2222 | ||||||
|  |     Hostname: localhost | ||||||
|  |     user: root | ||||||
|  |     IdentityFile: ./docker/backytest | ||||||
							
								
								
									
										99
									
								
								tests/packageCommands.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								tests/packageCommands.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | |||||||
|  | commands: | ||||||
|  |   checkDockerNoVersion: | ||||||
|  |     type: package | ||||||
|  |     shell: zsh | ||||||
|  |     packages:  | ||||||
|  |       - name: "docker-ce-cli" | ||||||
|  |       - name: "docker-ce" | ||||||
|  |     packageManager: apt | ||||||
|  |     packageOperation: checkVersion | ||||||
|  |  | ||||||
|  |   checkAptNoVersion: | ||||||
|  |     type: package | ||||||
|  |     shell: zsh | ||||||
|  |     packages:  | ||||||
|  |       - name: "apt" | ||||||
|  |     packageManager: apt | ||||||
|  |     packageOperation: checkVersion | ||||||
|  |  | ||||||
|  |   checkDockerPartialVersionWithoutRegex: | ||||||
|  |     type: package | ||||||
|  |     shell: zsh | ||||||
|  |     packages:  | ||||||
|  |       - name: "docker-ce-cli" | ||||||
|  |         version: "5:28.0.4-1" | ||||||
|  |       - name: "docker-ce" | ||||||
|  |         version: "5:28.0.4-1" | ||||||
|  |     packageManager: apt | ||||||
|  |     packageOperation: checkVersion | ||||||
|  |  | ||||||
|  |   checkDockerPartialVersionWithRegex: | ||||||
|  |     type: package | ||||||
|  |     shell: zsh | ||||||
|  |     packages:  | ||||||
|  |       - name: "docker-ce-cli" | ||||||
|  |         version: "5:28.0-*" | ||||||
|  |       - name: "docker-ce" | ||||||
|  |         version: '5:28\.0\.4-1~([A-Za-z0-9]+(\.[A-Za-z0-9]+)+)*' | ||||||
|  |     packageManager: apt | ||||||
|  |     packageOperation: checkVersion | ||||||
|  |      | ||||||
|  |   installJq: | ||||||
|  |     type: package | ||||||
|  |     shell: bash | ||||||
|  |     packages:  | ||||||
|  |       - name: "jq" | ||||||
|  |     packageManager: apt | ||||||
|  |     packageOperation: install | ||||||
|  |     output: | ||||||
|  |       toLog: true | ||||||
|  |  | ||||||
|  |   testJq: | ||||||
|  |     cmd: jq | ||||||
|  |     shell: bash | ||||||
|  |     Args:  | ||||||
|  |       - '--version' | ||||||
|  |     output: | ||||||
|  |       toLog: true | ||||||
|  |  | ||||||
|  |   checkDockerVersionWithInvalidRegex: | ||||||
|  |     type: package | ||||||
|  |     shell: zsh | ||||||
|  |     packages:  | ||||||
|  |       - name: "docker-ce-cli" | ||||||
|  |         version: "5:28.0-**" | ||||||
|  |       - name: "docker-ce" | ||||||
|  |         version: '5:28\.0\K.4-1~([A-Za-z0-9]+(\.[A-Za-z0-9]+)+)*' | ||||||
|  |     packageManager: apt | ||||||
|  |     packageOperation: checkVersion | ||||||
|  |  | ||||||
|  |   hostname: | ||||||
|  |     shell: zsh | ||||||
|  |     cmd: hostname | ||||||
|  |     Args: | ||||||
|  |       - '-f' | ||||||
|  |     output: | ||||||
|  |       toLog: true | ||||||
|  |  | ||||||
|  |  | ||||||
|  | cmdLists: | ||||||
|  |   packageCommands: | ||||||
|  |     output: | ||||||
|  |       onFailure: true | ||||||
|  |     order: | ||||||
|  |       - checkDockerPartialVersionWithoutRegex | ||||||
|  |       - checkDockerPartialVersionWithRegex | ||||||
|  |       - checkDockerVersionWithInvalidRegex | ||||||
|  |       - checkDockerNoVersion | ||||||
|  |        | ||||||
|  |   aptCommands: | ||||||
|  |     output: | ||||||
|  |       onFailure: true | ||||||
|  |     order: | ||||||
|  |       - installJq | ||||||
|  |       - checkDockerNoVersion | ||||||
|  |       - hostname | ||||||
|  |  | ||||||
|  | logging: | ||||||
|  |   verbose: true | ||||||
|  |   cmd-std-out: false | ||||||
							
								
								
									
										52
									
								
								tests/packageCommandsDNF.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								tests/packageCommandsDNF.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  | commands: | ||||||
|  |   checkDockerNoVersion: | ||||||
|  |     type: package | ||||||
|  |     shell: zsh | ||||||
|  |     packages:  | ||||||
|  |       - name: "docker-ce-cli" | ||||||
|  |       - name: "docker-ce" | ||||||
|  |     packageManager: dnf | ||||||
|  |     packageOperation: checkVersion | ||||||
|  |  | ||||||
|  |   checkDockerPartialVersionWithoutRegex: | ||||||
|  |     type: package | ||||||
|  |     shell: zsh | ||||||
|  |     packages:  | ||||||
|  |       - name: "docker-ce-cli" | ||||||
|  |         version: "5:28.0.4-1" | ||||||
|  |       - name: "docker-ce" | ||||||
|  |         version: "5:28.0.4-1" | ||||||
|  |     packageManager: dnf | ||||||
|  |     packageOperation: checkVersion | ||||||
|  |  | ||||||
|  |   checkDockerPartialVersionWithRegex: | ||||||
|  |     type: package | ||||||
|  |     shell: zsh | ||||||
|  |     packages:  | ||||||
|  |       - name: "docker-ce-cli" | ||||||
|  |         version: "5:28.0-*" | ||||||
|  |       - name: "docker-ce" | ||||||
|  |         version: '5:28\.0\.4-1~([A-Za-z0-9]+(\.[A-Za-z0-9]+)+)*' | ||||||
|  |     packageManager: dnf | ||||||
|  |     packageOperation: checkVersion | ||||||
|  |      | ||||||
|  |   checkDockerVersionWithInvalidRegex: | ||||||
|  |     type: package | ||||||
|  |     shell: zsh | ||||||
|  |     packages:  | ||||||
|  |       - name: "docker-ce-cli" | ||||||
|  |         version: "5:28.0-**" | ||||||
|  |       - name: "docker-ce" | ||||||
|  |         version: '5:28\.0\K.4-1~([A-Za-z0-9]+(\.[A-Za-z0-9]+)+)*' | ||||||
|  |     packageManager: dnf | ||||||
|  |     packageOperation: checkVersion | ||||||
|  |  | ||||||
|  | cmdLists: | ||||||
|  |   packageCommands: | ||||||
|  |     output: | ||||||
|  |       onFailure: true | ||||||
|  |     order: | ||||||
|  |       - checkDockerPartialVersionWithoutRegex | ||||||
|  |       - checkDockerPartialVersionWithRegex | ||||||
|  |       - checkDockerVersionWithInvalidRegex | ||||||
|  |       - checkDockerNoVersion | ||||||
		Reference in New Issue
	
	Block a user