diff --git a/.changes/unreleased/Added-20250409-174528.yaml b/.changes/unreleased/Added-20250409-174528.yaml deleted file mode 100755 index d99f574..0000000 --- a/.changes/unreleased/Added-20250409-174528.yaml +++ /dev/null @@ -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 diff --git a/.changes/unreleased/Added-20250501-110745.yaml b/.changes/unreleased/Added-20250501-110745.yaml deleted file mode 100755 index 07c06ff..0000000 --- a/.changes/unreleased/Added-20250501-110745.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: Added -body: 'Command lists: added `cmdLists.[name].notify` object' -time: 2025-05-01T11:07:45.96164753-05:00 diff --git a/.changes/unreleased/Added-20250704-085917.yaml b/.changes/unreleased/Added-20250704-085917.yaml deleted file mode 100755 index c77b535..0000000 --- a/.changes/unreleased/Added-20250704-085917.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: Added -body: Testing setup with Docker -time: 2025-07-04T08:59:17.430373451-05:00 diff --git a/.changes/unreleased/Added-20250704-102126.yaml b/.changes/unreleased/Added-20250704-102126.yaml deleted file mode 100755 index 6977170..0000000 --- a/.changes/unreleased/Added-20250704-102126.yaml +++ /dev/null @@ -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 diff --git a/.changes/unreleased/Added-20250715-202303.yaml b/.changes/unreleased/Added-20250715-202303.yaml deleted file mode 100755 index 66fe070..0000000 --- a/.changes/unreleased/Added-20250715-202303.yaml +++ /dev/null @@ -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 diff --git a/.changes/unreleased/Added-20250723-220340.yaml b/.changes/unreleased/Added-20250723-220340.yaml deleted file mode 100755 index 7828a89..0000000 --- a/.changes/unreleased/Added-20250723-220340.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: Added -body: 'CLI: added `exec hosts` subcommand `list`' -time: 2025-07-23T22:03:40.24191927-05:00 diff --git a/.changes/unreleased/Changed-20250321-090849.yaml b/.changes/unreleased/Changed-20250321-090849.yaml deleted file mode 100755 index 2d66733..0000000 --- a/.changes/unreleased/Changed-20250321-090849.yaml +++ /dev/null @@ -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 diff --git a/.changes/unreleased/Changed-20250325-003357.yaml b/.changes/unreleased/Changed-20250325-003357.yaml deleted file mode 100755 index 1183f42..0000000 --- a/.changes/unreleased/Changed-20250325-003357.yaml +++ /dev/null @@ -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 diff --git a/.changes/unreleased/Changed-20250407-223020.yaml b/.changes/unreleased/Changed-20250407-223020.yaml deleted file mode 100755 index d442825..0000000 --- a/.changes/unreleased/Changed-20250407-223020.yaml +++ /dev/null @@ -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 diff --git a/.changes/unreleased/Changed-20250418-133440.yaml b/.changes/unreleased/Changed-20250418-133440.yaml deleted file mode 100755 index adf4169..0000000 --- a/.changes/unreleased/Changed-20250418-133440.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: Changed -body: 'Internal: refactoring and renaming functions' -time: 2025-04-18T13:34:40.842541658-05:00 diff --git a/.changes/unreleased/Changed-20250501-110534.yaml b/.changes/unreleased/Changed-20250501-110534.yaml deleted file mode 100755 index ce59067..0000000 --- a/.changes/unreleased/Changed-20250501-110534.yaml +++ /dev/null @@ -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 diff --git a/.changes/unreleased/Changed-20250609-072601.yaml b/.changes/unreleased/Changed-20250609-072601.yaml deleted file mode 100755 index 5d83578..0000000 --- a/.changes/unreleased/Changed-20250609-072601.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: Changed -body: Change internal method name for better understanding -time: 2025-06-09T07:26:01.819927627-05:00 diff --git a/.changes/unreleased/Changed-20250709-231919.yaml b/.changes/unreleased/Changed-20250709-231919.yaml deleted file mode 100755 index f04fe2e..0000000 --- a/.changes/unreleased/Changed-20250709-231919.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: Changed -body: Improved error message for remote version package output -time: 2025-07-09T23:19:19.431960446-05:00 diff --git a/.changes/unreleased/Fixed-20250418-095747.yaml b/.changes/unreleased/Fixed-20250418-095747.yaml deleted file mode 100755 index 7e2a42a..0000000 --- a/.changes/unreleased/Fixed-20250418-095747.yaml +++ /dev/null @@ -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 diff --git a/.changes/unreleased/Fixed-20250424-225711.yaml b/.changes/unreleased/Fixed-20250424-225711.yaml deleted file mode 100755 index 1636aa7..0000000 --- a/.changes/unreleased/Fixed-20250424-225711.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: Fixed -body: Log file passed using `--log-file` correctly used -time: 2025-04-24T22:57:11.592829277-05:00 diff --git a/.changes/unreleased/Fixed-20251115-173206.yaml b/.changes/unreleased/Fixed-20251115-173206.yaml deleted file mode 100644 index 625344f..0000000 --- a/.changes/unreleased/Fixed-20251115-173206.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: Fixed -body: Cmd Type `script` now correctly appends arguments -time: 2025-11-15T17:32:06.86128885-06:00 diff --git a/.changes/v0.11.0.md b/.changes/v0.11.0.md new file mode 100644 index 0000000..8ecdffb --- /dev/null +++ b/.changes/v0.11.0.md @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index bf85aac..65b4145 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,28 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and is generated by [Changie](https://github.com/miniscruff/changie). +## 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 ### Added * Notifications: http service added diff --git a/cmd/version.go b/cmd/version.go index fae6d63..d86ad0d 100755 --- a/cmd/version.go +++ b/cmd/version.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/cobra" ) -const versionStr = "0.10.2" +const versionStr = "0.11.0" var ( versionCmd = &cobra.Command{ diff --git a/docs/content/config/commands/_index.md b/docs/content/config/commands/_index.md index e0a0d18..07b6f7c 100755 --- a/docs/content/config/commands/_index.md +++ b/docs/content/config/commands/_index.md @@ -19,7 +19,8 @@ Values available for this section **(case-sensitive)**: | `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 | | `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 | | `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 | @@ -51,7 +52,12 @@ Get command output when a notification is sent. Is not required. Can be `true` or `false`. -#### host +### host + + +{{% notice warning %}} +Depricated: use `hosts` instead. +{{% /notice %}} {{% notice info %}} 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. {{% /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 If shell is defined, the command will run in the specified shell. diff --git a/docs/content/config/commands/packages.md b/docs/content/config/commands/packages.md index 309a024..8ec65b3 100755 --- a/docs/content/config/commands/packages.md +++ b/docs/content/config/commands/packages.md @@ -8,7 +8,7 @@ This is dedicated to `package` commands. The command `type` field must be `packa | 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 | | `packageOperation` | The type of operation to perform. | `string` | yes | | `packageVersion` | The version of a package. | `string` | no | @@ -22,7 +22,9 @@ The following is an example of a package command: update-docker: type: package shell: zsh - packageName: docker-ce + packages: + - name: docker-ce + version: 10 packageManager: apt packageOperation: install host: debian-based-host diff --git a/examples/backy.yaml b/examples/backy.yaml index 17046dd..8c2153b 100755 --- a/examples/backy.yaml +++ b/examples/backy.yaml @@ -29,20 +29,22 @@ commands: update-docker: type: package shell: zsh # best to run package commands in a shell - packageName: docker-ce - Args: - - docker-ce-cli + packages: + - name: docker-ce + version: latest + - name: docker-ce-cli + version: latest packageManager: apt packageOperation: install update-dockerApt: # type: package shell: zsh cmd: apt - Args: - - update - - "&&" - - apt install -y docker-ce - - docker-ce-cli + packages: + - name: docker-ce + version: latest + - name: docker-ce-cli + version: latest packageManager: apt packageOperation: install diff --git a/examples/example.yml b/examples/example.yml index b11914f..51384a8 100755 --- a/examples/example.yml +++ b/examples/example.yml @@ -7,7 +7,8 @@ commands: - 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 + hosts: + - prod hooks: error: - some-other-command-when-failing diff --git a/pkg/backy/allowedexternaldirectives_enumer.go b/pkg/backy/allowedexternaldirectives_enumer.go old mode 100755 new mode 100644 diff --git a/pkg/backy/backy.go b/pkg/backy/backy.go index dca9484..cc3b5e3 100755 --- a/pkg/backy/backy.go +++ b/pkg/backy/backy.go @@ -145,6 +145,73 @@ func (e *LocalCommandExecutor) Run(cmd *Command, opts *ConfigOpts, logger zerolo 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. // The environment of local commands will be the machine's environment plus any extra // 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 ) + 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 command = getCommandTypeAndSetCommandInfo(command) diff --git a/pkg/backy/commandtype_enumer.go b/pkg/backy/commandtype_enumer.go old mode 100755 new mode 100644 diff --git a/pkg/backy/packageoperation_enumer.go b/pkg/backy/packageoperation_enumer.go old mode 100755 new mode 100644 diff --git a/pkg/backy/ssh.go b/pkg/backy/ssh.go index 247b268..3059d95 100755 --- a/pkg/backy/ssh.go +++ b/pkg/backy/ssh.go @@ -454,6 +454,10 @@ func (command *Command) RunCmdOnHost(cmdCtxLogger zerolog.Logger, opts *ConfigOp // cmdCtxLogger.Debug().Str("cmd", command.Cmd).Strs("args", command.Args).Send() // 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 err := command.RemoteHost.ConnectToHost(opts); err != nil { return nil, fmt.Errorf("failed to connect to host: %w", err) diff --git a/pkg/backy/types.go b/pkg/backy/types.go index 25567e2..bb31df7 100755 --- a/pkg/backy/types.go +++ b/pkg/backy/types.go @@ -57,7 +57,8 @@ type ( // See CommandType enum further down the page for acceptable values 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"`