Compare commits
13 Commits
55ef8e1733
...
v0.11.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 61add23efb | |||
| b228fca371 | |||
| e5a9003ed6 | |||
| 803b039849 | |||
| 2824f8c703 | |||
| cfc00262ff | |||
| fd019bc407 | |||
| febc2680f4 | |||
| caf2397349 | |||
| 172ca8712e | |||
| bda16bcbb5 | |||
| b5d069112f | |||
| f56393c84c |
@@ -1,3 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
kind: Added
|
|
||||||
body: 'Command lists: added `cmdLists.[name].notify` object'
|
|
||||||
time: 2025-05-01T11:07:45.96164753-05:00
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
kind: Added
|
|
||||||
body: Testing setup with Docker
|
|
||||||
time: 2025-07-04T08:59:17.430373451-05:00
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
kind: Added
|
|
||||||
body: 'CLI: Exec subcommand `hosts`. See documentation for more details.'
|
|
||||||
time: 2025-07-15T20:23:03.647128713-05:00
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
kind: Added
|
|
||||||
body: 'CLI: added `exec hosts` subcommand `list`'
|
|
||||||
time: 2025-07-23T22:03:40.24191927-05:00
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
kind: Changed
|
|
||||||
body: 'Internal: refactoring and renaming functions'
|
|
||||||
time: 2025-04-18T13:34:40.842541658-05:00
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
kind: Changed
|
|
||||||
body: 'Commands: moved output-prefixed keys to the `commands.[name].output` object'
|
|
||||||
time: 2025-05-01T11:05:34.90130087-05:00
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
kind: Changed
|
|
||||||
body: Change internal method name for better understanding
|
|
||||||
time: 2025-06-09T07:26:01.819927627-05:00
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
kind: Changed
|
|
||||||
body: Improved error message for remote version package output
|
|
||||||
time: 2025-07-09T23:19:19.431960446-05:00
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
kind: Fixed
|
|
||||||
body: 'Command Lists: hooks now run correctly when commands finish'
|
|
||||||
time: 2025-04-18T09:57:47.39035092-05:00
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
kind: Fixed
|
|
||||||
body: Log file passed using `--log-file` correctly used
|
|
||||||
time: 2025-04-24T22:57:11.592829277-05:00
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
kind: Fixed
|
|
||||||
body: Cmd Type `script` now correctly appends arguments
|
|
||||||
time: 2025-11-15T17:32:06.86128885-06:00
|
|
||||||
21
.changes/v0.11.0.md
Normal file
21
.changes/v0.11.0.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
## v0.11.0 - 2025-11-24
|
||||||
|
### Added
|
||||||
|
* feat: Package operation `versionCheck` supports regular expressions (see [regexp](https://pkg.go.dev/regexp) package for docs)
|
||||||
|
* Command lists: added `cmdLists.[name].notify` object
|
||||||
|
* Testing setup with Docker
|
||||||
|
* CLI: add global flag --hostsConfig that allows hosts to be dynamic in relation to the main config
|
||||||
|
* CLI: Exec subcommand `hosts`. See documentation for more details.
|
||||||
|
* CLI: added `exec hosts` subcommand `list`
|
||||||
|
* Add support for hosts in parallel
|
||||||
|
### Changed
|
||||||
|
* Commands: `host` can now be `localhost` or `127.0.0.1` to run commands locally
|
||||||
|
* lists loaded from external files only if no list config present in current file
|
||||||
|
* `PackageManager.Parse` renamed to `ParseRemotePackageManagerVersionOutput`. This now returns arrays of PackageManagerCommon.Package and errors.
|
||||||
|
* Internal: refactoring and renaming functions
|
||||||
|
* Commands: moved output-prefixed keys to the `commands.[name].output` object
|
||||||
|
* Change internal method name for better understanding
|
||||||
|
* Improved error message for remote version package output
|
||||||
|
### Fixed
|
||||||
|
* Command Lists: hooks now run correctly when commands finish
|
||||||
|
* Log file passed using `--log-file` correctly used
|
||||||
|
* Cmd Type `script` now correctly appends arguments
|
||||||
6
.changes/v0.11.1.md
Normal file
6
.changes/v0.11.1.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
## v0.11.1 - 2025-12-08
|
||||||
|
### Added
|
||||||
|
* Started integration testing
|
||||||
|
### Changed
|
||||||
|
* inject ssh env vars by apppending them to the script/command if SSH setenv fails
|
||||||
|
* fix local command injection by running in a shell
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
steps:
|
steps:
|
||||||
build:
|
build:
|
||||||
image: hugomods/hugo:debian-ci-0.146.0
|
image: hugomods/hugo:debian-ci-0.147.2
|
||||||
commands:
|
commands:
|
||||||
- git submodule foreach 'git fetch origin; git checkout $(git describe --tags `git rev-list --tags --max-count=1`);'
|
- git submodule foreach 'git fetch origin; git checkout $(git describe --tags `git rev-list --tags --max-count=1`);'
|
||||||
- cd docs
|
- cd docs
|
||||||
|
|||||||
29
CHANGELOG.md
29
CHANGELOG.md
@@ -6,6 +6,35 @@ 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.11.1 - 2025-12-08
|
||||||
|
### Added
|
||||||
|
* Started integration testing
|
||||||
|
### Changed
|
||||||
|
* inject ssh env vars by apppending them to the script/command if SSH setenv fails
|
||||||
|
* fix local command injection by running in a shell
|
||||||
|
|
||||||
|
## v0.11.0 - 2025-11-24
|
||||||
|
### Added
|
||||||
|
* feat: Package operation `versionCheck` supports regular expressions (see [regexp](https://pkg.go.dev/regexp) package for docs)
|
||||||
|
* Command lists: added `cmdLists.[name].notify` object
|
||||||
|
* Testing setup with Docker
|
||||||
|
* CLI: add global flag --hostsConfig that allows hosts to be dynamic in relation to the main config
|
||||||
|
* CLI: Exec subcommand `hosts`. See documentation for more details.
|
||||||
|
* CLI: added `exec hosts` subcommand `list`
|
||||||
|
* Add support for hosts in parallel
|
||||||
|
### Changed
|
||||||
|
* Commands: `host` can now be `localhost` or `127.0.0.1` to run commands locally
|
||||||
|
* lists loaded from external files only if no list config present in current file
|
||||||
|
* `PackageManager.Parse` renamed to `ParseRemotePackageManagerVersionOutput`. This now returns arrays of PackageManagerCommon.Package and errors.
|
||||||
|
* Internal: refactoring and renaming functions
|
||||||
|
* Commands: moved output-prefixed keys to the `commands.[name].output` object
|
||||||
|
* Change internal method name for better understanding
|
||||||
|
* Improved error message for remote version package output
|
||||||
|
### Fixed
|
||||||
|
* Command Lists: hooks now run correctly when commands finish
|
||||||
|
* Log file passed using `--log-file` correctly used
|
||||||
|
* Cmd Type `script` now correctly appends arguments
|
||||||
|
|
||||||
## v0.10.2 - 2025-03-19
|
## v0.10.2 - 2025-03-19
|
||||||
### Added
|
### Added
|
||||||
* Notifications: http service added
|
* Notifications: http service added
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
const versionStr = "0.10.2"
|
const versionStr = "0.11.1"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
versionCmd = &cobra.Command{
|
versionCmd = &cobra.Command{
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
baseURL: https://backy.cybershell.xyz/
|
baseURL: https://backy.cybershell.xyz/
|
||||||
languageCode: en-us
|
languageCode: en-us
|
||||||
title: A tool for commands
|
title: A tool for commands
|
||||||
ignoreLogs: 'warning-partial-superfluous-prefix'
|
|
||||||
theme:
|
theme:
|
||||||
- hugo-theme-relearn
|
- hugo-theme-relearn
|
||||||
- plausible-hugo
|
- plausible-hugo
|
||||||
|
|||||||
0
docs/content/cli/_index.md
Executable file → Normal file
0
docs/content/cli/_index.md
Executable file → Normal file
@@ -19,7 +19,8 @@ Values available for this section **(case-sensitive)**:
|
|||||||
| `environment` | Defines environment variables for the command | `[]string` | no | Partial |
|
| `environment` | Defines environment variables for the command | `[]string` | no | Partial |
|
||||||
| `type` | See documentation further down the page. Additional fields may be required. | `string` | no | 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 | No |
|
| `getOutput` | Command(s) output is in the notification(s) | `bool` | no | No |
|
||||||
| `host` | If not specified, the command will execute locally. | `string` | no | No |
|
| `host` | Depricated: use `hosts`. If not specified, the command will execute locally. | `string` | no | No |
|
||||||
|
| `hosts` | Must be specified to run commands both locallly and in parrallel. | `[]string` | no | No |
|
||||||
| `scriptEnvFile` | When type is `scriptFile` or `script`, this file is prepended to the input. | `string` | no | 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 | 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 | 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 |
|
||||||
@@ -51,7 +52,12 @@ Get command output when a notification is sent.
|
|||||||
|
|
||||||
Is not required. Can be `true` or `false`.
|
Is not required. Can be `true` or `false`.
|
||||||
|
|
||||||
#### host
|
### host
|
||||||
|
|
||||||
|
|
||||||
|
{{% notice warning %}}
|
||||||
|
Depricated: use `hosts` instead.
|
||||||
|
{{% /notice %}}
|
||||||
|
|
||||||
{{% notice info %}}
|
{{% notice info %}}
|
||||||
If any `host` is not defined or left blank, the command will run on the local machine.
|
If any `host` is not defined or left blank, the command will run on the local machine.
|
||||||
@@ -66,6 +72,36 @@ For example, say that I have a host defined in my SSH config with the `Host` def
|
|||||||
If I assign a value to host as `host: web-prod` and don't specify this value in the `hosts` object, web-prod will be used as the `Host` in searching the SSH config files.
|
If I assign a value to host as `host: web-prod` and don't specify this value in the `hosts` object, web-prod will be used as the `Host` in searching the SSH config files.
|
||||||
{{% /notice %}}
|
{{% /notice %}}
|
||||||
|
|
||||||
|
### hosts
|
||||||
|
|
||||||
|
{{% notice info %}}
|
||||||
|
If any `command.[name].hosts` index is `localhost` or `127.0.0.1`, the command will run on the local machine.
|
||||||
|
|
||||||
|
You can also remove the field to have the command run locally.
|
||||||
|
{{% /notice %}}
|
||||||
|
|
||||||
|
Host may or may not be defined in the `hosts` section.
|
||||||
|
|
||||||
|
{{% notice info %}}
|
||||||
|
If any `host` from the commands section does not match any object in the `hosts` section, the `Host` is assumed to be this value. This value will be used to search in the default SSH config files.
|
||||||
|
|
||||||
|
For example, say that I have a host defined in my SSH config with the `Host` defined as `web-prod`.
|
||||||
|
If I assign a value to host as `host: web-prod` and don't specify this value in the `hosts` object, web-prod will be used as the `Host` in searching the SSH config files.
|
||||||
|
{{% /notice %}}
|
||||||
|
|
||||||
|
###### Example:
|
||||||
|
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
command:
|
||||||
|
start-some-process:
|
||||||
|
cmd: start-server
|
||||||
|
hosts:
|
||||||
|
- prod-1
|
||||||
|
- prod-2
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### shell
|
### shell
|
||||||
|
|
||||||
If shell is defined, the command will run in the specified shell.
|
If shell is defined, the command will run in the specified shell.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ This is dedicated to `package` commands. The command `type` field must be `packa
|
|||||||
|
|
||||||
| name | notes | type | required |
|
| name | notes | type | required |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `packageName` | The name of a package to be modified. | `string` | yes |
|
| `packageName` | The name of a package to be modified. | `[]packagemanagercommon.Package` | yes |
|
||||||
| `packageManager` | The name of the package manger to be used. | `string` | yes |
|
| `packageManager` | The name of the package manger to be used. | `string` | yes |
|
||||||
| `packageOperation` | The type of operation to perform. | `string` | yes |
|
| `packageOperation` | The type of operation to perform. | `string` | yes |
|
||||||
| `packageVersion` | The version of a package. | `string` | no |
|
| `packageVersion` | The version of a package. | `string` | no |
|
||||||
@@ -22,7 +22,9 @@ The following is an example of a package command:
|
|||||||
update-docker:
|
update-docker:
|
||||||
type: package
|
type: package
|
||||||
shell: zsh
|
shell: zsh
|
||||||
packageName: docker-ce
|
packages:
|
||||||
|
- name: docker-ce
|
||||||
|
version: 10
|
||||||
packageManager: apt
|
packageManager: apt
|
||||||
packageOperation: install
|
packageOperation: install
|
||||||
host: debian-based-host
|
host: debian-based-host
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ External directives are for including data that should not be in the config file
|
|||||||
|
|
||||||
See the docs of each command if the field is supported.
|
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.
|
If the file path does not begin with the root directory marker, usually `/`, the config file's directory will be used as the starting point.
|
||||||
@@ -29,20 +29,22 @@ commands:
|
|||||||
update-docker:
|
update-docker:
|
||||||
type: package
|
type: package
|
||||||
shell: zsh # best to run package commands in a shell
|
shell: zsh # best to run package commands in a shell
|
||||||
packageName: docker-ce
|
packages:
|
||||||
Args:
|
- name: docker-ce
|
||||||
- docker-ce-cli
|
version: latest
|
||||||
|
- name: docker-ce-cli
|
||||||
|
version: latest
|
||||||
packageManager: apt
|
packageManager: apt
|
||||||
packageOperation: install
|
packageOperation: install
|
||||||
update-dockerApt:
|
update-dockerApt:
|
||||||
# type: package
|
# type: package
|
||||||
shell: zsh
|
shell: zsh
|
||||||
cmd: apt
|
cmd: apt
|
||||||
Args:
|
packages:
|
||||||
- update
|
- name: docker-ce
|
||||||
- "&&"
|
version: latest
|
||||||
- apt install -y docker-ce
|
- name: docker-ce-cli
|
||||||
- docker-ce-cli
|
version: latest
|
||||||
packageManager: apt
|
packageManager: apt
|
||||||
packageOperation: install
|
packageOperation: install
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ commands:
|
|||||||
- down
|
- down
|
||||||
# if host is not defined, command will be run locally
|
# if host is not defined, command will be run locally
|
||||||
# The host has to be defined in either the config file or the SSH Config files
|
# The host has to be defined in either the config file or the SSH Config files
|
||||||
host: some-host
|
hosts:
|
||||||
|
- prod
|
||||||
hooks:
|
hooks:
|
||||||
error:
|
error:
|
||||||
- some-other-command-when-failing
|
- some-other-command-when-failing
|
||||||
|
|||||||
0
pkg/backy/allowedexternaldirectives_enumer.go
Executable file → Normal file
0
pkg/backy/allowedexternaldirectives_enumer.go
Executable file → Normal file
@@ -145,6 +145,73 @@ func (e *LocalCommandExecutor) Run(cmd *Command, opts *ConfigOpts, logger zerolo
|
|||||||
return outputArr, nil
|
return outputArr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ensureRemoteHost ensures localCmd.RemoteHost is set for the given host.
|
||||||
|
// It prefers opts.Hosts lookup and falls back to a minimal Host entry so remote execution can proceed.
|
||||||
|
func (opts *ConfigOpts) ensureRemoteHost(localCmd *Command, host string) {
|
||||||
|
if localCmd.RemoteHost != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if opts != nil && opts.Hosts != nil {
|
||||||
|
if rh, found := opts.Hosts[host]; found {
|
||||||
|
localCmd.RemoteHost = rh
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// fallback: create a minimal Host so RunCmdOnHost sees a non-nil RemoteHost.
|
||||||
|
// This uses host as the address/alias; further fields (user/key) will use defaults.
|
||||||
|
localCmd.RemoteHost = &Host{Host: host}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecCommandOnHostsParallel runs a single configured command concurrently on the command.Hosts list.
|
||||||
|
// It reuses the standard RunCmd / RunCmdOnHost flow so the behavior is identical to normal execution.
|
||||||
|
func (opts *ConfigOpts) ExecCommandOnHostsParallel(cmdName string) ([]CmdResult, error) {
|
||||||
|
cmdObj, ok := opts.Cmds[cmdName]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("command %s not found", cmdName)
|
||||||
|
}
|
||||||
|
if len(cmdObj.Hosts) == 0 {
|
||||||
|
return nil, fmt.Errorf("no hosts configured for command %s", cmdName)
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
resultsCh := make(chan CmdResult, len(cmdObj.Hosts))
|
||||||
|
|
||||||
|
for _, host := range cmdObj.Hosts {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(h string) {
|
||||||
|
defer wg.Done()
|
||||||
|
// shallow copy to avoid races
|
||||||
|
local := *cmdObj
|
||||||
|
local.Host = h
|
||||||
|
opts.Logger.Debug().Str("host", h).Msg("executing command in parallel on host")
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if IsHostLocal(h) {
|
||||||
|
_, err := local.RunCmd(local.GenerateLogger(opts), opts)
|
||||||
|
resultsCh <- CmdResult{CmdName: cmdName, ListName: "", Error: err}
|
||||||
|
return
|
||||||
|
// _, err = local.RunCmd(local.GenerateLogger(opts), opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure RemoteHost is populated before calling RunCmdOnHost
|
||||||
|
opts.ensureRemoteHost(&local, h)
|
||||||
|
|
||||||
|
_, err = local.RunCmdOnHost(local.GenerateLogger(opts), opts)
|
||||||
|
|
||||||
|
resultsCh <- CmdResult{CmdName: cmdName, ListName: "", Error: err}
|
||||||
|
}(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
close(resultsCh)
|
||||||
|
|
||||||
|
var results []CmdResult
|
||||||
|
for r := range resultsCh {
|
||||||
|
results = append(results, r)
|
||||||
|
}
|
||||||
|
return results, 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.
|
||||||
@@ -167,6 +234,14 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
|
|||||||
outputArr []string // holds the output strings returned by processes
|
outputArr []string // holds the output strings returned by processes
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if command.Host != "" && command.Hosts != nil {
|
||||||
|
cmdCtxLogger.Warn().Msg("both 'host' and 'hosts' are set; 'hosts' will be ignored")
|
||||||
|
return nil, fmt.Errorf("both 'host' and 'hosts' are set; please set one or the other")
|
||||||
|
} else if command.Hosts != nil {
|
||||||
|
opts.ExecCommandOnHostsParallel(command.Name)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Getting the command type must be done before concatenating the arguments
|
// Getting the command type must be done before concatenating the arguments
|
||||||
command = getCommandTypeAndSetCommandInfo(command)
|
command = getCommandTypeAndSetCommandInfo(command)
|
||||||
|
|
||||||
@@ -275,7 +350,11 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
|
|||||||
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 {
|
||||||
localCMD = exec.Command(command.Cmd, command.Args...)
|
if command.Env != "" || command.Environment != nil {
|
||||||
|
localCMD = exec.Command("/bin/sh", "-c", ArgsStr)
|
||||||
|
} else {
|
||||||
|
localCMD = exec.Command(command.Cmd, command.Args...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
0
pkg/backy/commandtype_enumer.go
Executable file → Normal file
0
pkg/backy/commandtype_enumer.go
Executable file → Normal file
@@ -239,10 +239,10 @@ func setLoggingOptions(backyKoanf *koanf.Koanf, opts *ConfigOpts) {
|
|||||||
isVerboseLoggingSetInConfig := backyKoanf.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"
|
|
||||||
if opts.LogFilePath == "" && backyKoanf.Exists(getLoggingKeyFromConfig("file")) {
|
if opts.LogFilePath == "" && backyKoanf.Exists(getLoggingKeyFromConfig("file")) {
|
||||||
logFile = backyKoanf.String(getLoggingKeyFromConfig("file"))
|
opts.LogFilePath = backyKoanf.String(getLoggingKeyFromConfig("file"))
|
||||||
opts.LogFilePath = logFile
|
} else {
|
||||||
|
opts.LogFilePath = "./backy.log"
|
||||||
}
|
}
|
||||||
|
|
||||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ func (opts *ConfigOpts) ListCommandList(list string) {
|
|||||||
// bool for commands not found
|
// bool for commands not found
|
||||||
// gets set to false if a command is not found
|
// gets set to false if a command is not found
|
||||||
// set to true if the command is found
|
// set to true if the command is found
|
||||||
var listFound bool = false
|
var listFound bool
|
||||||
var listInfo *CmdList
|
var listInfo *CmdList
|
||||||
// check commands in file against cmd
|
// check commands in file against cmd
|
||||||
for listInFile, l := range opts.CmdConfigLists {
|
for listInFile, l := range opts.CmdConfigLists {
|
||||||
|
|||||||
@@ -1,67 +1,67 @@
|
|||||||
package backy
|
package backy
|
||||||
|
|
||||||
import (
|
// import (
|
||||||
"testing"
|
// "testing"
|
||||||
"time"
|
// "time"
|
||||||
)
|
// )
|
||||||
|
|
||||||
func TestAddingMetricsForCommand(t *testing.T) {
|
// func TestAddingMetricsForCommand(t *testing.T) {
|
||||||
|
|
||||||
// Create a new MetricFile
|
// // Create a new MetricFile
|
||||||
metricFile := NewMetricsFromFile("test_metrics.json")
|
// metricFile := NewMetricsFromFile("test_metrics.json")
|
||||||
|
|
||||||
metricFile, err := LoadMetricsFromFile(metricFile.Filename)
|
// metricFile, err := LoadMetricsFromFile(metricFile.Filename)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
t.Errorf("Failed to load metrics from file: %v", err)
|
// t.Errorf("Failed to load metrics from file: %v", err)
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Add metrics for a command
|
// // Add metrics for a command
|
||||||
commandName := "test_command"
|
// commandName := "test_command"
|
||||||
if _, exists := metricFile.CommandMetrics[commandName]; !exists {
|
// if _, exists := metricFile.CommandMetrics[commandName]; !exists {
|
||||||
metricFile.CommandMetrics[commandName] = NewMetrics()
|
// metricFile.CommandMetrics[commandName] = NewMetrics()
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Update the metrics for the command
|
// // Update the metrics for the command
|
||||||
executionTime := 1.8 // Example execution time in seconds
|
// executionTime := 1.8 // Example execution time in seconds
|
||||||
success := true // Example success status
|
// success := true // Example success status
|
||||||
metricFile.CommandMetrics[commandName].Update(success, executionTime, time.Now())
|
// metricFile.CommandMetrics[commandName].Update(success, executionTime, time.Now())
|
||||||
|
|
||||||
// Check if the metrics were updated correctly
|
// // Check if the metrics were updated correctly
|
||||||
if metricFile.CommandMetrics[commandName].SuccessfulExecutions > 50 {
|
// if metricFile.CommandMetrics[commandName].SuccessfulExecutions > 50 {
|
||||||
t.Errorf("Expected 1 successful execution, got %d", metricFile.CommandMetrics[commandName].SuccessfulExecutions)
|
// t.Errorf("Expected 1 successful execution, got %d", metricFile.CommandMetrics[commandName].SuccessfulExecutions)
|
||||||
}
|
// }
|
||||||
if metricFile.CommandMetrics[commandName].TotalExecutions > 50 {
|
// if metricFile.CommandMetrics[commandName].TotalExecutions > 50 {
|
||||||
t.Errorf("Expected 1 total execution, got %d", metricFile.CommandMetrics[commandName].TotalExecutions)
|
// t.Errorf("Expected 1 total execution, got %d", metricFile.CommandMetrics[commandName].TotalExecutions)
|
||||||
}
|
// }
|
||||||
// if metricFile.CommandMetrics[commandName].TotalExecutionTime != executionTime {
|
// // if metricFile.CommandMetrics[commandName].TotalExecutionTime != executionTime {
|
||||||
// t.Errorf("Expected execution time %f, got %f", executionTime, metricFile.CommandMetrics[commandName].TotalExecutionTime)
|
// // t.Errorf("Expected execution time %f, got %f", executionTime, metricFile.CommandMetrics[commandName].TotalExecutionTime)
|
||||||
// }
|
// // }
|
||||||
|
|
||||||
err = metricFile.SaveToFile()
|
// err = metricFile.SaveToFile()
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
t.Errorf("Failed to save metrics to file: %v", err)
|
// t.Errorf("Failed to save metrics to file: %v", err)
|
||||||
}
|
// }
|
||||||
|
|
||||||
listName := "test_list"
|
// listName := "test_list"
|
||||||
if _, exists := metricFile.ListMetrics[listName]; !exists {
|
// if _, exists := metricFile.ListMetrics[listName]; !exists {
|
||||||
metricFile.ListMetrics[listName] = NewMetrics()
|
// metricFile.ListMetrics[listName] = NewMetrics()
|
||||||
}
|
// }
|
||||||
// Update the metrics for the list
|
// // Update the metrics for the list
|
||||||
metricFile.ListMetrics[listName].Update(success, executionTime, time.Now())
|
// metricFile.ListMetrics[listName].Update(success, executionTime, time.Now())
|
||||||
if metricFile.ListMetrics[listName].SuccessfulExecutions > 50 {
|
// if metricFile.ListMetrics[listName].SuccessfulExecutions > 50 {
|
||||||
t.Errorf("Expected 1 successful execution for list, got %d", metricFile.ListMetrics[listName].SuccessfulExecutions)
|
// t.Errorf("Expected 1 successful execution for list, got %d", metricFile.ListMetrics[listName].SuccessfulExecutions)
|
||||||
}
|
// }
|
||||||
if metricFile.ListMetrics[listName].TotalExecutions > 50 {
|
// if metricFile.ListMetrics[listName].TotalExecutions > 50 {
|
||||||
t.Errorf("Expected 1 total execution for list, got %d", metricFile.ListMetrics[listName].TotalExecutions)
|
// t.Errorf("Expected 1 total execution for list, got %d", metricFile.ListMetrics[listName].TotalExecutions)
|
||||||
}
|
// }
|
||||||
// if metricFile.ListMetrics[listName].TotalExecutionTime > executionTime {
|
// // if metricFile.ListMetrics[listName].TotalExecutionTime > executionTime {
|
||||||
// t.Errorf("Expected execution time %f for list, got %f", executionTime, metricFile.ListMetrics[listName].TotalExecutionTime)
|
// // t.Errorf("Expected execution time %f for list, got %f", executionTime, metricFile.ListMetrics[listName].TotalExecutionTime)
|
||||||
// }
|
// // }
|
||||||
|
|
||||||
// Save the metrics to a file
|
// // Save the metrics to a file
|
||||||
err = metricFile.SaveToFile()
|
// err = metricFile.SaveToFile()
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
t.Errorf("Failed to save metrics to file: %v", err)
|
// t.Errorf("Failed to save metrics to file: %v", err)
|
||||||
}
|
// }
|
||||||
|
|
||||||
}
|
// }
|
||||||
|
|||||||
0
pkg/backy/packageoperation_enumer.go
Executable file → Normal file
0
pkg/backy/packageoperation_enumer.go
Executable file → Normal file
@@ -454,6 +454,10 @@ func (command *Command) RunCmdOnHost(cmdCtxLogger zerolog.Logger, opts *ConfigOp
|
|||||||
// cmdCtxLogger.Debug().Str("cmd", command.Cmd).Strs("args", command.Args).Send()
|
// cmdCtxLogger.Debug().Str("cmd", command.Cmd).Strs("args", command.Args).Send()
|
||||||
|
|
||||||
// Ensure SSH client is connected
|
// Ensure SSH client is connected
|
||||||
|
if command.RemoteHost == nil {
|
||||||
|
cmdCtxLogger.Err(fmt.Errorf("remote host is not defined for command %s", command.Name)).Send()
|
||||||
|
return nil, fmt.Errorf("remote host is not defined for command %s", command.Name)
|
||||||
|
}
|
||||||
if command.RemoteHost.SshClient == nil {
|
if command.RemoteHost.SshClient == nil {
|
||||||
if err := command.RemoteHost.ConnectToHost(opts); err != nil {
|
if err := command.RemoteHost.ConnectToHost(opts); err != nil {
|
||||||
return nil, fmt.Errorf("failed to connect to host: %w", err)
|
return nil, fmt.Errorf("failed to connect to host: %w", err)
|
||||||
@@ -467,9 +471,6 @@ func (command *Command) RunCmdOnHost(cmdCtxLogger zerolog.Logger, opts *ConfigOp
|
|||||||
}
|
}
|
||||||
defer commandSession.Close()
|
defer commandSession.Close()
|
||||||
|
|
||||||
// Inject environment variables
|
|
||||||
injectEnvIntoSSH(envVars, commandSession, opts, cmdCtxLogger)
|
|
||||||
|
|
||||||
// Set output writers
|
// Set output writers
|
||||||
cmdOutWriters = io.MultiWriter(&cmdOutBuf)
|
cmdOutWriters = io.MultiWriter(&cmdOutBuf)
|
||||||
if IsCmdStdOutEnabled() {
|
if IsCmdStdOutEnabled() {
|
||||||
@@ -478,6 +479,16 @@ func (command *Command) RunCmdOnHost(cmdCtxLogger zerolog.Logger, opts *ConfigOp
|
|||||||
commandSession.Stdout = cmdOutWriters
|
commandSession.Stdout = cmdOutWriters
|
||||||
commandSession.Stderr = cmdOutWriters
|
commandSession.Stderr = cmdOutWriters
|
||||||
|
|
||||||
|
command.ArgStr = fmt.Sprintf("%s %s", command.Cmd, ArgsStr)
|
||||||
|
//! environment vars and SSH:
|
||||||
|
//? skip if commandType is not *script*?
|
||||||
|
//? option to use SSH setenv or add to beginning?
|
||||||
|
// Inject environment variables
|
||||||
|
err = injectEnvIntoSSH(envVars, commandSession, opts, cmdCtxLogger)
|
||||||
|
if err != nil {
|
||||||
|
cmdCtxLogger.Info().Err(fmt.Errorf("%v; appending env variables to beginning of command", err)).Send()
|
||||||
|
command.ArgStr = prependEnvVarsToCommand(envVars, opts, command.Cmd, command.Args, cmdCtxLogger)
|
||||||
|
}
|
||||||
// Handle command execution based on type
|
// Handle command execution based on type
|
||||||
switch command.Type {
|
switch command.Type {
|
||||||
case ScriptCommandType:
|
case ScriptCommandType:
|
||||||
@@ -491,11 +502,15 @@ func (command *Command) RunCmdOnHost(cmdCtxLogger zerolog.Logger, opts *ConfigOp
|
|||||||
return remoteHostPackageExecutor.RunCmdOnHost(command, commandSession, cmdCtxLogger, cmdOutBuf)
|
return remoteHostPackageExecutor.RunCmdOnHost(command, commandSession, cmdCtxLogger, cmdOutBuf)
|
||||||
default:
|
default:
|
||||||
if command.Shell != "" {
|
if command.Shell != "" {
|
||||||
ArgsStr = fmt.Sprintf("%s -c '%s %s'", command.Shell, command.Cmd, ArgsStr)
|
command.ArgStr = fmt.Sprintf("%s -c '%s'", command.Shell, command.ArgStr)
|
||||||
} else {
|
} else {
|
||||||
ArgsStr = fmt.Sprintf("%s %s", command.Cmd, ArgsStr)
|
if command.Env == "" && command.Environment == nil {
|
||||||
|
// command.ArgStr = fmt.Sprintf("/bin/sh -c '%s'", command.ArgStr)
|
||||||
|
command.ArgStr = fmt.Sprintf("%s %s", command.Cmd, ArgsStr)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
cmdCtxLogger.Debug().Str("cmd + args", ArgsStr).Send()
|
// cmdCtxLogger.Debug().Str("cmd + args", ArgsStr).Send()
|
||||||
|
|
||||||
if command.Type == UserCommandType && command.UserOperation == "password" {
|
if command.Type == UserCommandType && command.UserOperation == "password" {
|
||||||
// cmdCtxLogger.Debug().Msgf("adding stdin")
|
// cmdCtxLogger.Debug().Msgf("adding stdin")
|
||||||
@@ -518,6 +533,7 @@ func (command *Command) RunCmdOnHost(cmdCtxLogger zerolog.Logger, opts *ConfigOp
|
|||||||
}
|
}
|
||||||
|
|
||||||
ArgsStr = fmt.Sprintf("cat %s | chpasswd", passFilePath)
|
ArgsStr = fmt.Sprintf("cat %s | chpasswd", passFilePath)
|
||||||
|
command.ArgStr = ArgsStr
|
||||||
defer passFile.Close()
|
defer passFile.Close()
|
||||||
|
|
||||||
rmFileFunc := func() {
|
rmFileFunc := func() {
|
||||||
@@ -526,7 +542,7 @@ func (command *Command) RunCmdOnHost(cmdCtxLogger zerolog.Logger, opts *ConfigOp
|
|||||||
|
|
||||||
defer rmFileFunc()
|
defer rmFileFunc()
|
||||||
}
|
}
|
||||||
if err := commandSession.Run(ArgsStr); err != nil {
|
if err := commandSession.Run(command.ArgStr); 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), fmt.Errorf("error running command: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -602,11 +618,10 @@ func checkPackageVersion(cmdCtxLogger zerolog.Logger, command *Command, commandS
|
|||||||
for _, v := range command.Args {
|
for _, v := range command.Args {
|
||||||
ArgsStr += fmt.Sprintf(" %s", v)
|
ArgsStr += fmt.Sprintf(" %s", v)
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
var cmdOut []byte
|
var cmdOut []byte
|
||||||
|
|
||||||
if cmdOut, err = commandSession.CombinedOutput(ArgsStr); err != nil {
|
if cmdOut, err = commandSession.CombinedOutput(command.ArgStr); err != nil {
|
||||||
cmdOutBuf.Write(cmdOut)
|
cmdOutBuf.Write(cmdOut)
|
||||||
|
|
||||||
_, parseErr := parsePackageVersion(string(cmdOut), cmdCtxLogger, command, cmdOutBuf)
|
_, parseErr := parsePackageVersion(string(cmdOut), cmdCtxLogger, command, cmdOutBuf)
|
||||||
@@ -669,6 +684,11 @@ func (command *Command) runScriptFile(session *ssh.Session, cmdCtxLogger zerolog
|
|||||||
func (command *Command) prepareScriptBuffer() (*bytes.Buffer, error) {
|
func (command *Command) prepareScriptBuffer() (*bytes.Buffer, error) {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
|
for _, envVar := range command.Environment {
|
||||||
|
buffer.WriteString(fmt.Sprintf("export %s", envVar))
|
||||||
|
buffer.WriteByte('\n')
|
||||||
|
}
|
||||||
|
|
||||||
if command.ScriptEnvFile != "" {
|
if command.ScriptEnvFile != "" {
|
||||||
envBuffer, err := readFileToBuffer(command.ScriptEnvFile)
|
envBuffer, err := readFileToBuffer(command.ScriptEnvFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -690,6 +710,11 @@ func (command *Command) prepareScriptBuffer() (*bytes.Buffer, error) {
|
|||||||
func (command *Command) prepareScriptFileBuffer() (*bytes.Buffer, error) {
|
func (command *Command) prepareScriptFileBuffer() (*bytes.Buffer, error) {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
|
for _, envVar := range command.Environment {
|
||||||
|
buffer.WriteString(fmt.Sprintf("export %s", envVar))
|
||||||
|
buffer.WriteByte('\n')
|
||||||
|
}
|
||||||
|
|
||||||
// Handle script environment file
|
// Handle script environment file
|
||||||
if command.ScriptEnvFile != "" {
|
if command.ScriptEnvFile != "" {
|
||||||
envBuffer, err := readFileToBuffer(command.ScriptEnvFile)
|
envBuffer, err := readFileToBuffer(command.ScriptEnvFile)
|
||||||
@@ -832,7 +857,7 @@ func (r RemoteHostPackageExecutor) RunCmdOnHost(command *Command, commandSession
|
|||||||
return checkPackageVersion(cmdCtxLogger, command, commandSession, cmdOutBuf)
|
return checkPackageVersion(cmdCtxLogger, command, commandSession, cmdOutBuf)
|
||||||
}
|
}
|
||||||
if command.Shell != "" {
|
if command.Shell != "" {
|
||||||
ArgsStr = fmt.Sprintf("%s -c '%s %s'", command.Shell, command.Cmd, ArgsStr)
|
ArgsStr = fmt.Sprintf("%s -c '%s'", command.Shell, command.ArgStr)
|
||||||
} else {
|
} else {
|
||||||
ArgsStr = fmt.Sprintf("%s %s", command.Cmd, ArgsStr)
|
ArgsStr = fmt.Sprintf("%s %s", command.Cmd, ArgsStr)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ type (
|
|||||||
// 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 string `yaml:"host,omitempty"`
|
Host string `yaml:"host,omitempty"`
|
||||||
|
Hosts []string `yaml:"hosts,omitempty"`
|
||||||
|
|
||||||
Hooks *Hooks `yaml:"hooks,omitempty"`
|
Hooks *Hooks `yaml:"hooks,omitempty"`
|
||||||
|
|
||||||
@@ -67,7 +68,8 @@ type (
|
|||||||
|
|
||||||
RemoteHost *Host `yaml:"-"`
|
RemoteHost *Host `yaml:"-"`
|
||||||
|
|
||||||
Args []string `yaml:"args,omitempty"`
|
Args []string `yaml:"args,omitempty"`
|
||||||
|
ArgStr string
|
||||||
|
|
||||||
Dir *string `yaml:"dir,omitempty"`
|
Dir *string `yaml:"dir,omitempty"`
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"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"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"mvdan.cc/sh/v3/shell"
|
"mvdan.cc/sh/v3/shell"
|
||||||
)
|
)
|
||||||
@@ -99,7 +100,7 @@ func NewConfigOptions(configFilePath string, opts ...BackyOptionFunc) *ConfigOpt
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
func injectEnvIntoSSH(envVarsToInject environmentVars, process *ssh.Session, opts *ConfigOpts, log zerolog.Logger) {
|
func injectEnvIntoSSH(envVarsToInject environmentVars, session *ssh.Session, opts *ConfigOpts, log zerolog.Logger) error {
|
||||||
if envVarsToInject.file != "" {
|
if envVarsToInject.file != "" {
|
||||||
envPath, envPathErr := getFullPathWithHomeDir(envVarsToInject.file)
|
envPath, envPathErr := getFullPathWithHomeDir(envVarsToInject.file)
|
||||||
if envPathErr != nil {
|
if envPathErr != nil {
|
||||||
@@ -113,31 +114,31 @@ func injectEnvIntoSSH(envVarsToInject environmentVars, process *ssh.Session, opt
|
|||||||
|
|
||||||
envMap, err := godotenv.Parse(file)
|
envMap, err := godotenv.Parse(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Str("envFile", envPath).Err(err).Send()
|
log.Fatal().Str("envFile", envPath).Err(err).Send()
|
||||||
goto errEnvFile
|
|
||||||
}
|
}
|
||||||
for key, val := range envMap {
|
for key, val := range envMap {
|
||||||
err = process.Setenv(key, getExternalConfigDirectiveValue(val, opts, AllowedExternalDirectiveVault))
|
err = session.Setenv(key, getExternalConfigDirectiveValue(val, opts, AllowedExternalDirectiveVault))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Send()
|
log.Info().Err(err).Send()
|
||||||
|
return fmt.Errorf("failed to set environment variable %s: %w", val, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
errEnvFile:
|
|
||||||
// fmt.Printf("%v", envVarsToInject.env)
|
// fmt.Printf("%v", envVarsToInject.env)
|
||||||
for _, envVal := range envVarsToInject.env {
|
for _, envVal := range envVarsToInject.env {
|
||||||
// don't append env Vars for Backy
|
// don't append env Vars for Backy
|
||||||
if strings.Contains(envVal, "=") {
|
if strings.Contains(envVal, "=") {
|
||||||
envVarArr := strings.Split(envVal, "=")
|
envVarArr := strings.Split(envVal, "=")
|
||||||
|
|
||||||
err := process.Setenv(envVarArr[0], getExternalConfigDirectiveValue(envVarArr[1], opts, AllowedExternalDirectiveVaultFile))
|
err := session.Setenv(envVarArr[0], getExternalConfigDirectiveValue(envVarArr[1], opts, AllowedExternalDirectiveVaultFile))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Send()
|
log.Info().Err(err).Send()
|
||||||
|
return fmt.Errorf("failed to set environment variable %s: %w", envVarArr[1], err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func injectEnvIntoLocalCMD(envVarsToInject environmentVars, process *exec.Cmd, log zerolog.Logger, opts *ConfigOpts) {
|
func injectEnvIntoLocalCMD(envVarsToInject environmentVars, process *exec.Cmd, log zerolog.Logger, opts *ConfigOpts) {
|
||||||
@@ -171,6 +172,35 @@ errEnvFile:
|
|||||||
process.Env = append(process.Env, os.Environ()...)
|
process.Env = append(process.Env, os.Environ()...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func prependEnvVarsToCommand(envVars environmentVars, opts *ConfigOpts, command string, args []string, cmdCtxLogger zerolog.Logger) string {
|
||||||
|
var envPrefix string
|
||||||
|
if envVars.file != "" {
|
||||||
|
envPath, envPathErr := getFullPathWithHomeDir(envVars.file)
|
||||||
|
if envPathErr != nil {
|
||||||
|
cmdCtxLogger.Fatal().Str("envFile", envPath).Err(envPathErr).Send()
|
||||||
|
}
|
||||||
|
file, err := os.Open(envPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Str("envFile", envPath).Err(err).Send()
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
envMap, err := godotenv.Parse(file)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Str("envFile", envPath).Err(err).Send()
|
||||||
|
}
|
||||||
|
for key, val := range envMap {
|
||||||
|
envPrefix += fmt.Sprintf("%s=%s ", key, getExternalConfigDirectiveValue(val, opts, AllowedExternalDirectiveVaultEnv))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, value := range envVars.env {
|
||||||
|
envVarArr := strings.Split(value, "=")
|
||||||
|
envPrefix += fmt.Sprintf("%s=%s ", envVarArr[0], getExternalConfigDirectiveValue(envVarArr[1], opts, AllowedExternalDirectiveVault))
|
||||||
|
envPrefix += "\n"
|
||||||
|
}
|
||||||
|
return envPrefix + command + " " + strings.Join(args, " ")
|
||||||
|
}
|
||||||
|
|
||||||
func contains(s []string, e string) bool {
|
func contains(s []string, e string) bool {
|
||||||
for _, a := range s {
|
for _, a := range s {
|
||||||
if a == e {
|
if a == e {
|
||||||
@@ -403,24 +433,22 @@ func getExternalConfigDirectiveValue(key string, opts *ConfigOpts, allowedDirect
|
|||||||
key = replaceVarInString(opts.Vars, key, opts.Logger)
|
key = replaceVarInString(opts.Vars, key, opts.Logger)
|
||||||
opts.Logger.Debug().Str("expanding external key", key).Send()
|
opts.Logger.Debug().Str("expanding external key", key).Send()
|
||||||
|
|
||||||
if strings.HasPrefix(key, envExternDirectiveStart) {
|
if newKeyStr, directiveFound := strings.CutPrefix(key, envExternDirectiveStart); directiveFound {
|
||||||
if IsExternalDirectiveEnv(allowedDirectives) {
|
if IsExternalDirectiveEnv(allowedDirectives) {
|
||||||
|
|
||||||
key = strings.TrimPrefix(key, envExternDirectiveStart)
|
key = strings.TrimSuffix(newKeyStr, externDirectiveEnd)
|
||||||
key = strings.TrimSuffix(key, externDirectiveEnd)
|
|
||||||
key = os.Getenv(key)
|
key = os.Getenv(key)
|
||||||
} else {
|
} else {
|
||||||
opts.Logger.Error().Msgf("Config key with value %s does not support env directive", key)
|
opts.Logger.Error().Msgf("Config key with value %s does not support env directive", key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(key, externFileDirectiveStart) {
|
if newKeyStr, directiveFound := strings.CutPrefix(key, externFileDirectiveStart); directiveFound {
|
||||||
if IsExternalDirectiveFile(allowedDirectives) {
|
if IsExternalDirectiveFile(allowedDirectives) {
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
var keyValue []byte
|
var keyValue []byte
|
||||||
key = strings.TrimPrefix(key, externFileDirectiveStart)
|
key = strings.TrimSuffix(newKeyStr, externDirectiveEnd)
|
||||||
key = strings.TrimSuffix(key, externDirectiveEnd)
|
|
||||||
key, err = getFullPathWithHomeDir(key)
|
key, err = getFullPathWithHomeDir(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
opts.Logger.Err(err).Send()
|
opts.Logger.Err(err).Send()
|
||||||
@@ -440,11 +468,10 @@ func getExternalConfigDirectiveValue(key string, opts *ConfigOpts, allowedDirect
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(key, vaultExternDirectiveStart) {
|
if newKeyStr, directiveFound := strings.CutPrefix(key, vaultExternDirectiveStart); directiveFound {
|
||||||
if IsExternalDirectiveVault(allowedDirectives) {
|
if IsExternalDirectiveVault(allowedDirectives) {
|
||||||
|
|
||||||
key = strings.TrimPrefix(key, vaultExternDirectiveStart)
|
key = strings.TrimSuffix(newKeyStr, externDirectiveEnd)
|
||||||
key = strings.TrimSuffix(key, externDirectiveEnd)
|
|
||||||
key = GetVaultKey(key, opts, opts.Logger)
|
key = GetVaultKey(key, opts, opts.Logger)
|
||||||
} else {
|
} else {
|
||||||
opts.Logger.Error().Msgf("Config key with value %s does not support vault directive", key)
|
opts.Logger.Error().Msgf("Config key with value %s does not support vault directive", key)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ commands:
|
|||||||
success:
|
success:
|
||||||
- successCmd
|
- successCmd
|
||||||
|
|
||||||
errorCmd:
|
successCmd:
|
||||||
name: get docker version
|
name: get docker version
|
||||||
cmd: docker
|
cmd: docker
|
||||||
getOutput: true
|
getOutput: true
|
||||||
|
|||||||
22
tests/files_test.go
Normal file
22
tests/files_test.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRunCommandFileTest(t *testing.T) {
|
||||||
|
filePath := "packageCommands.yml"
|
||||||
|
cmdLineStr := fmt.Sprintf("go run ../backy.go exec host -c checkDockerNoVersion -m localhost --cmdStdOut -f %s", filePath)
|
||||||
|
|
||||||
|
cmd := exec.Command("bash", "-c", cmdLineStr)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Command failed: %v, Output: %s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(output) == 0 {
|
||||||
|
t.Fatal("Expected command output, got none")
|
||||||
|
}
|
||||||
|
}
|
||||||
61
tests/integration_test.go
Normal file
61
tests/integration_test.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIntegration_ExecuteCommand(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
expectFail bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Version Command",
|
||||||
|
args: []string{"version"},
|
||||||
|
expectFail: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid Command",
|
||||||
|
args: []string{"invalid"},
|
||||||
|
expectFail: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cmd := exec.Command("go", append([]string{"run", "../backy.go"}, tt.args...)...)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
|
||||||
|
if tt.expectFail && err == nil {
|
||||||
|
t.Fatalf("Expected failure but got success. Output: %s", string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tt.expectFail && err != nil {
|
||||||
|
t.Fatalf("Expected success but got failure. Error: %v, Output: %s", err, string(output))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntegration_ExecuteCommandWithConfig(t *testing.T) {
|
||||||
|
configFile := "./SuccessHook.yml"
|
||||||
|
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||||
|
t.Fatalf("Config file not found: %s", configFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("go", "run", "../backy.go", "exec", "--config", configFile, "echoTestSuccess")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Command execution failed. Error: %v, Output: %s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(output) == 0 {
|
||||||
|
t.Fatal("Expected command output, got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Command output: %s", string(output))
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ commands:
|
|||||||
checkDockerNoVersion:
|
checkDockerNoVersion:
|
||||||
type: package
|
type: package
|
||||||
shell: zsh
|
shell: zsh
|
||||||
|
environment:
|
||||||
|
- TEST_ENV=production
|
||||||
packages:
|
packages:
|
||||||
- name: "docker-ce-cli"
|
- name: "docker-ce-cli"
|
||||||
- name: "docker-ce"
|
- name: "docker-ce"
|
||||||
|
|||||||
14
tests/run_tests.sh
Normal file
14
tests/run_tests.sh
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# This script runs all Go test files in the tests directory.
|
||||||
|
|
||||||
|
echo "Running all tests in the tests directory..."
|
||||||
|
|
||||||
|
go test ./tests/... -v
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "All tests passed successfully."
|
||||||
|
else
|
||||||
|
echo "Some tests failed. Check the output above for details."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user