Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b2d89352a3 | |||
| 765ef2ee36 | |||
| 995e4f91b5 | |||
| fa62bc1ec6 | |||
| 2766ac997a | |||
| d0b4c0b9df | |||
| beabe9f041 | |||
| 3a038eeab4 | |||
| a95f903e72 | |||
| 61add23efb | |||
| b228fca371 | |||
| e5a9003ed6 | |||
| 803b039849 | |||
| 2824f8c703 | |||
| cfc00262ff |
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
|
||||||
3
.changes/v0.11.2.md
Normal file
3
.changes/v0.11.2.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
## v0.11.2 - 2025-12-27
|
||||||
|
### Added
|
||||||
|
* Upgraded GoCron; web ui viewer for viewing cron jobs
|
||||||
7
.changes/v0.11.3.md
Normal file
7
.changes/v0.11.3.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
## v0.11.3 - 2026-01-31
|
||||||
|
### Added
|
||||||
|
* Command: saveShellHistory for scriptFile commands over SSH
|
||||||
|
* Starting on Variables and Templates
|
||||||
|
### Changed
|
||||||
|
* File output for commands now adds hostname to beginning of filename
|
||||||
|
* Testing: docker testing infra
|
||||||
3
.changes/v0.11.4.md
Normal file
3
.changes/v0.11.4.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
## v0.11.4 - 2026-02-01
|
||||||
|
### Changed
|
||||||
|
* Command.[name].output.file: now appends correctly to the beginning of file in an absolute path
|
||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
- run: git fetch --force --tags
|
- run: git fetch --force --tags
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.23'
|
go-version: '1.24'
|
||||||
cache: true
|
cache: true
|
||||||
# More assembly might be required: Docker logins, GPG, etc. It all depends
|
# More assembly might be required: Docker logins, GPG, etc. It all depends
|
||||||
# on your needs.
|
# on your needs.
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ snapshot:
|
|||||||
version_template: "{{ incpatch .Version }}-next"
|
version_template: "{{ incpatch .Version }}-next"
|
||||||
changelog:
|
changelog:
|
||||||
disable: false
|
disable: false
|
||||||
|
release:
|
||||||
|
prerelease: auto
|
||||||
|
|
||||||
gitea_urls:
|
gitea_urls:
|
||||||
api: https://git.andrewnw.xyz/api/v1
|
api: https://git.andrewnw.xyz/api/v1
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ archives:
|
|||||||
formats: [zip]
|
formats: [zip]
|
||||||
checksum:
|
checksum:
|
||||||
name_template: 'checksums.txt'
|
name_template: 'checksums.txt'
|
||||||
|
release:
|
||||||
|
prerelease: auto
|
||||||
snapshot:
|
snapshot:
|
||||||
version_template: "{{ incpatch .Version }}-next"
|
version_template: "{{ incpatch .Version }}-next"
|
||||||
changelog:
|
changelog:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
steps:
|
steps:
|
||||||
golang:
|
golang:
|
||||||
image: golang:1.23
|
image: golang:1.24
|
||||||
commands:
|
commands:
|
||||||
- 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"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
steps:
|
steps:
|
||||||
build:
|
build:
|
||||||
image: golang
|
image: golang:1.24
|
||||||
commands:
|
commands:
|
||||||
- go build
|
- go build
|
||||||
- go test
|
- go test
|
||||||
19
.woodpecker/github/github.yml
Normal file
19
.woodpecker/github/github.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
steps:
|
||||||
|
golang:
|
||||||
|
image: golang:1.24
|
||||||
|
commands:
|
||||||
|
- go install github.com/goreleaser/goreleaser/v2@v2.7.0
|
||||||
|
- goreleaser release -f .goreleaser/github.yml --release-notes=".changes/$(go run backy.go version -V).md"
|
||||||
|
environment:
|
||||||
|
GITHUB_TOKEN:
|
||||||
|
from_secret: github_token
|
||||||
|
|
||||||
|
when:
|
||||||
|
event: tag
|
||||||
|
# release:
|
||||||
|
# image: goreleaser/goreleaser
|
||||||
|
# commands:
|
||||||
|
|
||||||
|
when:
|
||||||
|
- event: tag
|
||||||
|
branch: master
|
||||||
23
CHANGELOG.md
23
CHANGELOG.md
@@ -6,6 +6,29 @@ 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.4 - 2026-02-01
|
||||||
|
### Changed
|
||||||
|
* Command.[name].output.file: now appends correctly to the beginning of file in an absolute path
|
||||||
|
|
||||||
|
## v0.11.3 - 2026-01-31
|
||||||
|
### Added
|
||||||
|
* Command: saveShellHistory for scriptFile commands over SSH
|
||||||
|
* Starting on Variables and Templates
|
||||||
|
### Changed
|
||||||
|
* File output for commands now adds hostname to beginning of filename
|
||||||
|
* Testing: docker testing infra
|
||||||
|
|
||||||
|
## v0.11.2 - 2025-12-27
|
||||||
|
### Added
|
||||||
|
* Upgraded GoCron; web ui viewer for viewing cron jobs
|
||||||
|
|
||||||
|
## 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
|
## v0.11.0 - 2025-11-24
|
||||||
### Added
|
### Added
|
||||||
* feat: Package operation `versionCheck` supports regular expressions (see [regexp](https://pkg.go.dev/regexp) package for docs)
|
* feat: Package operation `versionCheck` supports regular expressions (see [regexp](https://pkg.go.dev/regexp) package for docs)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
const versionStr = "0.11.0"
|
const versionStr = "0.11.4"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
versionCmd = &cobra.Command{
|
versionCmd = &cobra.Command{
|
||||||
|
|||||||
@@ -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.
|
||||||
49
docs/content/config/gocron.md
Normal file
49
docs/content/config/gocron.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
title: "Configuring Cron"
|
||||||
|
weight: 3
|
||||||
|
description: >
|
||||||
|
Use Cron to run lists at a specified time.
|
||||||
|
---
|
||||||
|
|
||||||
|
Backy provides an easy-to-use way to execute commands at a specified time.
|
||||||
|
|
||||||
|
Adding `cron: 0 0 1 * * *` to a `cmdLists` object will schedule the list at 1 in the morning. See [https://crontab.guru/](https://crontab.guru/) for reference.
|
||||||
|
|
||||||
|
GoCron allows one to configure a server to view the jobs in the scheduler. See [GoCron UI GitHub](https://github.com/go-co-op/gocron-ui).
|
||||||
|
GoCron can be configured or left alone for defaults.
|
||||||
|
|
||||||
|
GoCron configuration:
|
||||||
|
|
||||||
|
| key | description | type | required | default
|
||||||
|
| --- | --- | --- | --- | ---
|
||||||
|
| `bindAddress` | Interface's IP to bind to. Must not contain port. | `string` | no | `:port`
|
||||||
|
| `port` | Port to use. | `int` | no | `8888`
|
||||||
|
| `useSeconds` | Whether to parse the second cron field. | `bool` | no | `false`
|
||||||
|
|
||||||
|
|
||||||
|
```yaml {lineNos="true" wrap="true" title="yaml"}
|
||||||
|
goCron:
|
||||||
|
bindAddress: "0.0.0.0"
|
||||||
|
port: 8888
|
||||||
|
useSeconds: true
|
||||||
|
|
||||||
|
cmdLists:
|
||||||
|
docker-container-backup: # this can be any name you want
|
||||||
|
# all commands have to be defined
|
||||||
|
order:
|
||||||
|
- stop-docker-container
|
||||||
|
- backup-docker-container-script
|
||||||
|
- shell-cmd
|
||||||
|
- hostname
|
||||||
|
- start-docker-container
|
||||||
|
notifications:
|
||||||
|
- matrix.id
|
||||||
|
name: backup-some-container
|
||||||
|
cron: "0 0 1 * * *"
|
||||||
|
hostname:
|
||||||
|
name: hostname
|
||||||
|
order:
|
||||||
|
- hostname
|
||||||
|
notifications:
|
||||||
|
- mail.prod-email
|
||||||
|
```
|
||||||
14
go.mod
14
go.mod
@@ -1,13 +1,12 @@
|
|||||||
module git.andrewnw.xyz/CyberShell/backy
|
module git.andrewnw.xyz/CyberShell/backy
|
||||||
|
|
||||||
go 1.23.0
|
go 1.24.0
|
||||||
|
|
||||||
toolchain go1.23.7
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.83.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-ui v0.2.0
|
||||||
|
github.com/go-co-op/gocron/v2 v2.19.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/hashicorp/vault/api v1.20.0
|
github.com/hashicorp/vault/api v1.20.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
@@ -50,6 +49,8 @@ require (
|
|||||||
github.com/go-jose/go-jose/v4 v4.1.1 // indirect
|
github.com/go-jose/go-jose/v4 v4.1.1 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.3.0 // 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/gorilla/mux v1.8.1 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.3 // 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
|
||||||
@@ -60,6 +61,7 @@ require (
|
|||||||
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
|
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.1-vault-7 // 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/jonboulle/clockwork v0.5.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.18.0 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
@@ -75,18 +77,18 @@ require (
|
|||||||
github.com/philhofer/fwd v1.2.0 // 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/cors v1.11.1 // indirect
|
||||||
github.com/rs/xid v1.6.0 // indirect
|
github.com/rs/xid v1.6.0 // indirect
|
||||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.6 // indirect
|
github.com/spf13/pflag v1.0.6 // indirect
|
||||||
github.com/stretchr/objx v0.5.2 // indirect
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
github.com/stretchr/testify v1.10.0 // indirect
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
github.com/tidwall/gjson v1.18.0 // indirect
|
github.com/tidwall/gjson v1.18.0 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
github.com/tidwall/sjson v1.2.5 // indirect
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
github.com/tinylib/msgp v1.3.0 // indirect
|
github.com/tinylib/msgp v1.3.0 // indirect
|
||||||
go.mau.fi/util v0.8.8 // indirect
|
go.mau.fi/util v0.8.8 // indirect
|
||||||
go.uber.org/atomic v1.11.0 // indirect
|
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect
|
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect
|
||||||
golang.org/x/mod v0.26.0 // indirect
|
golang.org/x/mod v0.26.0 // indirect
|
||||||
|
|||||||
38
go.sum
38
go.sum
@@ -26,7 +26,6 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3
|
|||||||
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.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -36,8 +35,10 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
|||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||||
github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0=
|
github.com/go-co-op/gocron-ui v0.2.0 h1:f4JqnIfgzeWYgJcNT5ukn86mnyewbXswsa1To1XQroc=
|
||||||
github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY=
|
github.com/go-co-op/gocron-ui v0.2.0/go.mod h1:QvFWbaoVY2fHVzQ3DvYdfFTSz22PaKFtNQuT7rXnj4Y=
|
||||||
|
github.com/go-co-op/gocron/v2 v2.19.0 h1:OKf2y6LXPs/BgBI2fl8PxUpNAI1DA9Mg+hSeGOS38OU=
|
||||||
|
github.com/go-co-op/gocron/v2 v2.19.0/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
|
||||||
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.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI=
|
github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI=
|
||||||
@@ -54,9 +55,12 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x
|
|||||||
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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
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.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=
|
||||||
|
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
@@ -84,6 +88,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
|
|||||||
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=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||||
|
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
|
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
|
||||||
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=
|
||||||
@@ -103,13 +109,8 @@ github.com/knadh/koanf/v2 v2.2.2 h1:ghbduIkpFui3L587wavneC9e3WIliCgiCgdxYO/wd7A=
|
|||||||
github.com/knadh/koanf/v2 v2.2.2/go.mod h1:abWQc0cBXLSF/PSOMCB/SK+T13NXDsPvOksbpi5e/9Q=
|
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 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
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.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.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
@@ -139,7 +140,6 @@ github.com/pascaldekloe/name v1.0.1 h1:9lnXOHeqeHHnWLbKfH6X98+4+ETVqFqxN09UXSjcM
|
|||||||
github.com/pascaldekloe/name v1.0.1/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 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
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/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 h1:4NGkvGudBL7GteO3m6qnaQ4pC0Kvf0onSVc9gR3EWBw=
|
||||||
@@ -148,10 +148,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
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.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
|
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||||
|
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||||
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.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
@@ -167,15 +167,12 @@ 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=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
|
||||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
@@ -191,9 +188,8 @@ github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57
|
|||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
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 h1:OnuEEc/sIJFhnq4kFggiImUpcmnmL/xpvQMRu5Fiy5c=
|
||||||
go.mau.fi/util v0.8.8/go.mod h1:Y/kS3loxTEhy8Vill513EtPXr+CRDdae+Xj2BXXMy/c=
|
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/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
@@ -281,10 +277,8 @@ 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/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||||
gopkg.in/yaml.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=
|
||||||
|
|||||||
@@ -14,10 +14,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
"embed"
|
"embed"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
"gopkg.in/natefinch/lumberjack.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed templates/*.txt
|
//go:embed templates/*.txt
|
||||||
@@ -65,10 +67,10 @@ func (e *PackageCommandExecutor) Run(cmd *Command, opts *ConfigOpts, logger zero
|
|||||||
|
|
||||||
// Execute the package version command
|
// Execute the package version command
|
||||||
execCmd := exec.Command(cmd.Cmd, cmd.Args...)
|
execCmd := exec.Command(cmd.Cmd, cmd.Args...)
|
||||||
cmdOutWriters = io.MultiWriter(&cmdOutBuf)
|
var err error
|
||||||
|
cmdOutWriters, _, err = makeCmdOutWriters(&cmdOutBuf, "")
|
||||||
if IsCmdStdOutEnabled() {
|
if err != nil {
|
||||||
cmdOutWriters = io.MultiWriter(os.Stdout, &cmdOutBuf)
|
return nil, err
|
||||||
}
|
}
|
||||||
execCmd.Stdout = cmdOutWriters
|
execCmd.Stdout = cmdOutWriters
|
||||||
execCmd.Stderr = cmdOutWriters
|
execCmd.Stderr = cmdOutWriters
|
||||||
@@ -145,6 +147,48 @@ func (e *LocalCommandExecutor) Run(cmd *Command, opts *ConfigOpts, logger zerolo
|
|||||||
return outputArr, nil
|
return outputArr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// makeCmdOutWriters constructs an io.Writer that writes to the provided buffer,
|
||||||
|
// optionally also to stdout and/or a file. If a file path is provided the
|
||||||
|
// caller is responsible for closing the returned *os.File when non-nil.
|
||||||
|
func makeCmdOutWriters(buf *bytes.Buffer, outputFile string) (io.Writer, *os.File, error) {
|
||||||
|
writers := io.MultiWriter(buf)
|
||||||
|
if IsCmdStdOutEnabled() {
|
||||||
|
|
||||||
|
console := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC1123}
|
||||||
|
console.FormatLevel = func(i interface{}) string {
|
||||||
|
return strings.ToUpper(fmt.Sprintf("| %-6s|", i))
|
||||||
|
}
|
||||||
|
console.FormatMessage = func(i any) string {
|
||||||
|
if i == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("MSG: %s", i)
|
||||||
|
}
|
||||||
|
console.FormatFieldName = func(i interface{}) string {
|
||||||
|
return fmt.Sprintf("%s: ", i)
|
||||||
|
}
|
||||||
|
console.FormatFieldValue = func(i interface{}) string {
|
||||||
|
return fmt.Sprintf("%s", i)
|
||||||
|
// return strings.ToUpper(fmt.Sprintf("%s", i))
|
||||||
|
}
|
||||||
|
|
||||||
|
writers = io.MultiWriter(console, writers)
|
||||||
|
}
|
||||||
|
if outputFile != "" {
|
||||||
|
|
||||||
|
fileLogger := &lumberjack.Logger{
|
||||||
|
MaxSize: 50, // megabytes
|
||||||
|
MaxBackups: 3,
|
||||||
|
MaxAge: 28, //days
|
||||||
|
Compress: false, // disabled by default
|
||||||
|
}
|
||||||
|
fileLogger.Filename = outputFile
|
||||||
|
|
||||||
|
writers = io.MultiWriter(fileLogger, writers)
|
||||||
|
}
|
||||||
|
return writers, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ensureRemoteHost ensures localCmd.RemoteHost is set for the given host.
|
// 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.
|
// 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) {
|
func (opts *ConfigOpts) ensureRemoteHost(localCmd *Command, host string) {
|
||||||
@@ -250,6 +294,7 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
|
|||||||
}
|
}
|
||||||
|
|
||||||
if command.Type == UserCommandType {
|
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")
|
||||||
}
|
}
|
||||||
@@ -283,23 +328,13 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
|
|||||||
localCMD = exec.Command(command.Shell, command.Args...)
|
localCMD = exec.Command(command.Shell, command.Args...)
|
||||||
injectEnvIntoLocalCMD(envVars, localCMD, cmdCtxLogger, opts)
|
injectEnvIntoLocalCMD(envVars, localCMD, cmdCtxLogger, opts)
|
||||||
|
|
||||||
cmdOutWriters = io.MultiWriter(&cmdOutBuf)
|
var outFile *os.File
|
||||||
|
cmdOutWriters, outFile, err := makeCmdOutWriters(&cmdOutBuf, command.Output.File)
|
||||||
if IsCmdStdOutEnabled() {
|
|
||||||
cmdOutWriters = io.MultiWriter(os.Stdout, &cmdOutBuf)
|
|
||||||
}
|
|
||||||
if command.Output.File != "" {
|
|
||||||
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, err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
if outFile != nil {
|
||||||
cmdOutWriters = io.MultiWriter(file, &cmdOutBuf)
|
defer outFile.Close()
|
||||||
|
|
||||||
if IsCmdStdOutEnabled() {
|
|
||||||
cmdOutWriters = io.MultiWriter(os.Stdout, file, &cmdOutBuf)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
localCMD.Stdin = bytes.NewReader(script)
|
localCMD.Stdin = bytes.NewReader(script)
|
||||||
@@ -349,10 +384,14 @@ 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 {
|
||||||
|
if command.Env != "" || command.Environment != nil {
|
||||||
|
localCMD = exec.Command("/bin/sh", "-c", ArgsStr)
|
||||||
} else {
|
} else {
|
||||||
localCMD = exec.Command(command.Cmd, command.Args...)
|
localCMD = exec.Command(command.Cmd, command.Args...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if command.Type == UserCommandType {
|
if command.Type == UserCommandType {
|
||||||
if command.UserOperation == "password" {
|
if command.UserOperation == "password" {
|
||||||
@@ -366,10 +405,9 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
|
|||||||
|
|
||||||
injectEnvIntoLocalCMD(envVars, localCMD, cmdCtxLogger, opts)
|
injectEnvIntoLocalCMD(envVars, localCMD, cmdCtxLogger, opts)
|
||||||
|
|
||||||
cmdOutWriters = io.MultiWriter(&cmdOutBuf)
|
cmdOutWriters, _, err = makeCmdOutWriters(&cmdOutBuf, "")
|
||||||
|
if err != nil {
|
||||||
if IsCmdStdOutEnabled() {
|
return outputArr, err
|
||||||
cmdOutWriters = io.MultiWriter(os.Stdout, &cmdOutBuf)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
localCMD.Stdout = cmdOutWriters
|
localCMD.Stdout = cmdOutWriters
|
||||||
@@ -954,6 +992,19 @@ func (cmd *Command) GenerateLogger(opts *ConfigOpts) zerolog.Logger {
|
|||||||
return cmdLogger
|
return cmdLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cmd *Command) GenerateLoggerForCmd(logger zerolog.Logger) zerolog.Logger {
|
||||||
|
cmdLogger := logger.With().
|
||||||
|
Str("Backy-cmd", cmd.Name).Str("Host", "local machine").
|
||||||
|
Logger()
|
||||||
|
|
||||||
|
if !IsHostLocal(cmd.Host) {
|
||||||
|
cmdLogger = logger.With().
|
||||||
|
Str("Backy-cmd", cmd.Name).Str("Host", cmd.Host).
|
||||||
|
Logger()
|
||||||
|
}
|
||||||
|
return cmdLogger
|
||||||
|
}
|
||||||
|
|
||||||
func (opts *ConfigOpts) ExecCmdsOnHosts(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 {
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const _CommandTypeName = "scriptscriptFileremoteScriptpackageuser"
|
const _CommandTypeName = "scriptscriptFileremoteScriptpackageuserfile"
|
||||||
|
|
||||||
var _CommandTypeIndex = [...]uint8{0, 0, 6, 16, 28, 35, 39}
|
var _CommandTypeIndex = [...]uint8{0, 0, 6, 16, 28, 35, 39, 43}
|
||||||
|
|
||||||
const _CommandTypeLowerName = "scriptscriptfileremotescriptpackageuser"
|
const _CommandTypeLowerName = "scriptscriptfileremotescriptpackageuserfile"
|
||||||
|
|
||||||
func (i CommandType) String() string {
|
func (i CommandType) String() string {
|
||||||
if i < 0 || i >= CommandType(len(_CommandTypeIndex)-1) {
|
if i < 0 || i >= CommandType(len(_CommandTypeIndex)-1) {
|
||||||
@@ -31,9 +31,10 @@ func _CommandTypeNoOp() {
|
|||||||
_ = x[RemoteScriptCommandType-(3)]
|
_ = x[RemoteScriptCommandType-(3)]
|
||||||
_ = x[PackageCommandType-(4)]
|
_ = x[PackageCommandType-(4)]
|
||||||
_ = x[UserCommandType-(5)]
|
_ = x[UserCommandType-(5)]
|
||||||
|
_ = x[FileCommandType-(6)]
|
||||||
}
|
}
|
||||||
|
|
||||||
var _CommandTypeValues = []CommandType{DefaultCommandType, ScriptCommandType, ScriptFileCommandType, RemoteScriptCommandType, PackageCommandType, UserCommandType}
|
var _CommandTypeValues = []CommandType{DefaultCommandType, ScriptCommandType, ScriptFileCommandType, RemoteScriptCommandType, PackageCommandType, UserCommandType, FileCommandType}
|
||||||
|
|
||||||
var _CommandTypeNameToValueMap = map[string]CommandType{
|
var _CommandTypeNameToValueMap = map[string]CommandType{
|
||||||
_CommandTypeName[0:0]: DefaultCommandType,
|
_CommandTypeName[0:0]: DefaultCommandType,
|
||||||
@@ -48,6 +49,8 @@ var _CommandTypeNameToValueMap = map[string]CommandType{
|
|||||||
_CommandTypeLowerName[28:35]: PackageCommandType,
|
_CommandTypeLowerName[28:35]: PackageCommandType,
|
||||||
_CommandTypeName[35:39]: UserCommandType,
|
_CommandTypeName[35:39]: UserCommandType,
|
||||||
_CommandTypeLowerName[35:39]: UserCommandType,
|
_CommandTypeLowerName[35:39]: UserCommandType,
|
||||||
|
_CommandTypeName[39:43]: FileCommandType,
|
||||||
|
_CommandTypeLowerName[39:43]: FileCommandType,
|
||||||
}
|
}
|
||||||
|
|
||||||
var _CommandTypeNames = []string{
|
var _CommandTypeNames = []string{
|
||||||
@@ -57,6 +60,7 @@ var _CommandTypeNames = []string{
|
|||||||
_CommandTypeName[16:28],
|
_CommandTypeName[16:28],
|
||||||
_CommandTypeName[28:35],
|
_CommandTypeName[28:35],
|
||||||
_CommandTypeName[35:39],
|
_CommandTypeName[35:39],
|
||||||
|
_CommandTypeName[39:43],
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommandTypeString retrieves an enum value from the enum constants string name.
|
// CommandTypeString retrieves an enum value from the enum constants string name.
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ func (opts *ConfigOpts) ParseConfigurationFile() *ConfigOpts {
|
|||||||
logging.ExitWithMSG("No cron fields detected in any command lists", 1, nil)
|
logging.ExitWithMSG("No cron fields detected in any command lists", 1, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unmarshalConfigIntoStruct(backyKoanf, "goCron", &opts.GoCron, opts.Logger)
|
||||||
|
|
||||||
if err := processCmds(opts); err != nil {
|
if err := processCmds(opts); err != nil {
|
||||||
logging.ExitWithMSG(err.Error(), 1, &opts.Logger)
|
logging.ExitWithMSG(err.Error(), 1, &opts.Logger)
|
||||||
}
|
}
|
||||||
@@ -425,6 +427,9 @@ func generateFileFetchErrorString(file, fileType string, err error) string {
|
|||||||
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 {
|
||||||
|
if cmdList.Name == "" {
|
||||||
|
cmdList.Name = cmdListName
|
||||||
|
}
|
||||||
// if cron is enabled and cron is not set, delete the list
|
// if cron is enabled and cron is not set, delete the list
|
||||||
if opts.cronEnabled && strings.TrimSpace(cmdList.Cron) == "" {
|
if opts.cronEnabled && strings.TrimSpace(cmdList.Cron) == "" {
|
||||||
opts.Logger.Debug().Str("cron", "enabled").Str("list", cmdListName).Msg("cron not set, deleting list")
|
opts.Logger.Debug().Str("cron", "enabled").Str("list", cmdListName).Msg("cron not set, deleting list")
|
||||||
@@ -544,7 +549,6 @@ func processCmds(opts *ConfigOpts) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !IsHostLocal(cmd.Host) {
|
if !IsHostLocal(cmd.Host) {
|
||||||
|
|
||||||
cmdHost := replaceVarInString(opts.Vars, cmd.Host, opts.Logger)
|
cmdHost := replaceVarInString(opts.Vars, cmd.Host, opts.Logger)
|
||||||
if cmdHost != cmd.Host {
|
if cmdHost != cmd.Host {
|
||||||
cmd.Host = cmdHost
|
cmd.Host = cmdHost
|
||||||
|
|||||||
@@ -6,30 +6,73 @@ package backy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.andrewnw.xyz/CyberShell/backy/pkg/logging"
|
"git.andrewnw.xyz/CyberShell/backy/pkg/logging"
|
||||||
"github.com/go-co-op/gocron"
|
"github.com/go-co-op/gocron-ui/server"
|
||||||
|
"github.com/go-co-op/gocron/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var defaultPort = 8888
|
||||||
|
|
||||||
func (opts *ConfigOpts) Cron() {
|
func (opts *ConfigOpts) Cron() {
|
||||||
s := gocron.NewScheduler(time.Local)
|
s, _ := gocron.NewScheduler(gocron.WithLocation(time.Local))
|
||||||
s.TagsUnique()
|
defer func() { _ = s.Shutdown() }()
|
||||||
|
opts.Logger.Info().Msg("Starting cron mode...")
|
||||||
|
s.Start()
|
||||||
cmdLists := opts.CmdConfigLists
|
cmdLists := opts.CmdConfigLists
|
||||||
for _, config := range cmdLists {
|
for _, config := range cmdLists {
|
||||||
|
|
||||||
cron := strings.TrimSpace(config.Cron)
|
cron := strings.TrimSpace(config.Cron)
|
||||||
if cron != "" {
|
if cron != "" {
|
||||||
opts.Logger.Info().Str("Scheduling cron list", config.Name).Str("Time", cron).Send()
|
job, err := s.NewJob(
|
||||||
_, err := s.CronWithSeconds(cron).Tag(config.Name).Do(func(cron string) {
|
gocron.CronJob(cron, opts.GoCron.UseSeconds),
|
||||||
opts.RunListConfig(cron)
|
gocron.NewTask(
|
||||||
}, cron)
|
func(cronStr string) {
|
||||||
|
opts.RunListConfig(cronStr)
|
||||||
|
},
|
||||||
|
cron,
|
||||||
|
),
|
||||||
|
gocron.WithName(config.Name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.ExitWithMSG(fmt.Sprintf("error: %v", err), 1, &opts.Logger)
|
logging.ExitWithMSG(fmt.Sprintf("error: %v", err), 1, &opts.Logger)
|
||||||
}
|
}
|
||||||
|
nextRun, _ := job.NextRun()
|
||||||
|
opts.Logger.Info().Str("Scheduling cron list", config.Name).Str("Time", cron).Str("Next run", nextRun.String()).Send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
opts.Logger.Info().Msg("Starting cron mode...")
|
|
||||||
s.StartBlocking()
|
// start the web UI server
|
||||||
|
if opts.GoCron.BindAddress == "" {
|
||||||
|
if opts.GoCron.Port == 0 {
|
||||||
|
opts.GoCron.BindAddress = ":8888"
|
||||||
|
} else {
|
||||||
|
opts.GoCron.BindAddress = fmt.Sprintf(":%d", opts.GoCron.Port)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if opts.GoCron.Port != 0 {
|
||||||
|
opts.GoCron.BindAddress = fmt.Sprintf("%s:%d", opts.GoCron.BindAddress, opts.GoCron.Port)
|
||||||
|
} else {
|
||||||
|
opts.GoCron.BindAddress = fmt.Sprintf("%s:%d", opts.GoCron.BindAddress, defaultPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// consensus := externalip.DefaultConsensus(nil, nil)
|
||||||
|
|
||||||
|
// By default Ipv4 or Ipv6 is returned,
|
||||||
|
// use the function below to limit yourself to IPv4,
|
||||||
|
// or pass in `6` instead to limit yourself to IPv6.
|
||||||
|
// consensus.UseIPProtocol(4)
|
||||||
|
|
||||||
|
// Get your IP,
|
||||||
|
// which is never <nil> when err is <nil>.
|
||||||
|
// ip, err := consensus.ExternalIP()
|
||||||
|
// if err == nil {
|
||||||
|
// fmt.Println(ip.String()) // print IPv4/IPv6 in string format
|
||||||
|
// }
|
||||||
|
srv := server.NewServer(s, opts.GoCron.Port)
|
||||||
|
// srv := server.NewServer(scheduler, 8080, server.WithTitle("My Custom Scheduler")) // with custom title if you want to customize the title of the UI (optional)
|
||||||
|
opts.Logger.Info().Msgf("GoCron UI available at http://%s", opts.GoCron.BindAddress)
|
||||||
|
opts.Logger.Fatal().Msg(http.ListenAndServe(opts.GoCron.BindAddress, srv.Router).Error())
|
||||||
|
select {} // wait forever
|
||||||
}
|
}
|
||||||
|
|||||||
193
pkg/backy/file.go
Normal file
193
pkg/backy/file.go
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
package backy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/pkg/sftp"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LocalFileCommandExecutor struct{}
|
||||||
|
|
||||||
|
func (f *LocalFileCommandExecutor) Execute(cmd *Command) error {
|
||||||
|
|
||||||
|
localExecutor := LocalCommandExecutor{}
|
||||||
|
|
||||||
|
switch cmd.FileOperation {
|
||||||
|
case "copy":
|
||||||
|
return localExecutor.copyFile(cmd.Source, cmd.Destination, cmd.Permissions)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported file operation: %s", cmd.FileOperation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *LocalFileCommandExecutor) ReadLocalFile(path string) ([]byte, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *LocalFileCommandExecutor) WriteLocalFile(path string, data []byte, Perms fs.FileMode) error {
|
||||||
|
err := os.WriteFile(path, data, Perms)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *LocalCommandExecutor) copyFile(source, destination string, Perms fs.FileMode) error {
|
||||||
|
input, err := os.ReadFile(source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = os.WriteFile(destination, input, Perms)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemoteFileCommandExecutor struct{}
|
||||||
|
|
||||||
|
func (r *RemoteFileCommandExecutor) Execute(cmd *Command) error {
|
||||||
|
|
||||||
|
remoteExecutor := RemoteFileCommandExecutor{}
|
||||||
|
sourceTypeLocal := false
|
||||||
|
|
||||||
|
if cmd.SourceType == "local" {
|
||||||
|
sourceTypeLocal = true
|
||||||
|
}
|
||||||
|
switch cmd.FileOperation {
|
||||||
|
case "copy":
|
||||||
|
return remoteExecutor.copyFile(cmd.Source, cmd.Destination, cmd.Permissions, cmd.RemoteHost.SshClient, sourceTypeLocal)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported file operation: %s", cmd.FileOperation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteFileCommandExecutor) copyFile(source, destination string, Perms fs.FileMode, sshClient *ssh.Client, sourceTypeLocal bool) error {
|
||||||
|
if sourceTypeLocal {
|
||||||
|
input, err := os.ReadFile(source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sftpClient, sftpErr := sftp.NewClient(sshClient)
|
||||||
|
if sftpErr != nil {
|
||||||
|
return sftpErr
|
||||||
|
}
|
||||||
|
defer sftpClient.Close()
|
||||||
|
|
||||||
|
destFile, err := sftpClient.Create(destination)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer destFile.Close()
|
||||||
|
|
||||||
|
_, err = destFile.Write(input)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = destFile.Chmod(Perms)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := sftp.NewClient(sshClient)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
srcFile, err := client.Open(source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer srcFile.Close()
|
||||||
|
|
||||||
|
destFile, err := client.Create(destination)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer destFile.Close()
|
||||||
|
|
||||||
|
_, err = srcFile.WriteTo(destFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = destFile.Chmod(Perms)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteFileCommandExecutor) ReadRemoteFile(path string, sshClient *ssh.Client) ([]byte, error) {
|
||||||
|
sftpClient, sftpErr := sftp.NewClient(sshClient)
|
||||||
|
if sftpErr != nil {
|
||||||
|
return nil, sftpErr
|
||||||
|
}
|
||||||
|
defer sftpClient.Close()
|
||||||
|
|
||||||
|
file, err := sftpClient.Open(path)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
var fileData []byte
|
||||||
|
|
||||||
|
_, err = file.Read(fileData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fileData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteFileCommandExecutor) WriteRemoteFile(path string, data []byte, Perms fs.FileMode, sshClient *ssh.Client) error {
|
||||||
|
sftpClient, sftpErr := sftp.NewClient(sshClient)
|
||||||
|
if sftpErr != nil {
|
||||||
|
return sftpErr
|
||||||
|
}
|
||||||
|
defer sftpClient.Close()
|
||||||
|
|
||||||
|
file, err := sftpClient.Create(path)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, err = file.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = file.Chmod(Perms)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileCommandExecutor interface {
|
||||||
|
Execute(cmd *Command) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFileCommandExecutor(isRemote bool) FileCommandExecutor {
|
||||||
|
if isRemote {
|
||||||
|
return &RemoteFileCommandExecutor{}
|
||||||
|
}
|
||||||
|
return &LocalFileCommandExecutor{}
|
||||||
|
}
|
||||||
78
pkg/backy/file_test.go
Normal file
78
pkg/backy/file_test.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package backy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCopyFileLocal(t *testing.T) {
|
||||||
|
FileCommand := Command{}
|
||||||
|
FileCommand.Type = FileCommandType
|
||||||
|
FileCommand.FileOperation = "copy"
|
||||||
|
FileCommand.Permissions = 0644
|
||||||
|
FileCommand.Source = "/home/andrew/Projects/backy/tests/data/fileops/source.txt"
|
||||||
|
FileCommand.Destination = "/home/andrew/Projects/backy/tests/data/fileops/destination.txt"
|
||||||
|
var FileCommandExecutor = LocalFileCommandExecutor{}
|
||||||
|
err := FileCommandExecutor.Execute(&FileCommand)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error executing file command: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
srcBytes, srcErr := os.ReadFile(FileCommand.Source)
|
||||||
|
if srcErr != nil {
|
||||||
|
t.Errorf("Error reading source file: %v", srcErr)
|
||||||
|
}
|
||||||
|
destBytes, destErr := os.ReadFile(FileCommand.Destination)
|
||||||
|
if destErr != nil {
|
||||||
|
t.Errorf("Error reading destination file: %v", destErr)
|
||||||
|
}
|
||||||
|
if string(srcBytes) != string(destBytes) {
|
||||||
|
t.Errorf("Source and destination files do not match")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional checks can be added here to verify the file was copied correctly
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCopyFileRemote(t *testing.T) {
|
||||||
|
opts := NewConfigOptions("")
|
||||||
|
opts.Hosts = map[string]*Host{}
|
||||||
|
RemoteFileCommand := Command{}
|
||||||
|
RemoteFileCommand.Type = FileCommandType
|
||||||
|
RemoteFileCommand.FileOperation = "copy"
|
||||||
|
RemoteFileCommand.Permissions = 0644
|
||||||
|
RemoteFileCommand.Destination = "/home/backy/destination.txt"
|
||||||
|
RemoteFileCommand.Source = "/home/andrew/Projects/backy/tests/data/fileops/source.txt"
|
||||||
|
RemoteFileCommand.Host = "localhost"
|
||||||
|
RemoteFileCommand.RemoteHost = &Host{
|
||||||
|
HostName: "localhost",
|
||||||
|
User: "backy",
|
||||||
|
Password: "backy",
|
||||||
|
Port: 2222,
|
||||||
|
PrivateKeyPath: "/home/andrew/Projects/backy/tests/docker/backytest",
|
||||||
|
KnownHostsFile: "/home/andrew/Projects/backy/tests/docker/known_hosts",
|
||||||
|
}
|
||||||
|
|
||||||
|
sshErr := RemoteFileCommand.RemoteHost.ConnectToHost(opts)
|
||||||
|
if sshErr != nil {
|
||||||
|
t.Errorf("Error connecting to remote host: %v", sshErr)
|
||||||
|
}
|
||||||
|
var RemoteFileCommandExecutor = RemoteFileCommandExecutor{}
|
||||||
|
err := RemoteFileCommandExecutor.Execute(&RemoteFileCommand)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error executing remote file command: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
srcBytes, srcErr := os.ReadFile(RemoteFileCommand.Source)
|
||||||
|
if srcErr != nil {
|
||||||
|
t.Errorf("Error reading source file: %v", srcErr)
|
||||||
|
}
|
||||||
|
destBytes, destErr := RemoteFileCommandExecutor.ReadRemoteFile(RemoteFileCommand.Destination, RemoteFileCommand.RemoteHost.SshClient)
|
||||||
|
if destErr != nil {
|
||||||
|
t.Errorf("Error reading destination file: %v", destErr)
|
||||||
|
}
|
||||||
|
if string(srcBytes) != string(destBytes) {
|
||||||
|
t.Errorf("Source and destination files do not match")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional checks can be added here to verify the file was copied correctly
|
||||||
|
}
|
||||||
141
pkg/backy/filecommandoperation_enumer.go
Normal file
141
pkg/backy/filecommandoperation_enumer.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
// Code generated by "enumer -linecomment -yaml -text -json -type=FileCommandOperation"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package backy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const _FileCommandOperationName = "copymovedeletechownchmod"
|
||||||
|
|
||||||
|
var _FileCommandOperationIndex = [...]uint8{0, 0, 4, 8, 14, 19, 24}
|
||||||
|
|
||||||
|
const _FileCommandOperationLowerName = "copymovedeletechownchmod"
|
||||||
|
|
||||||
|
func (i FileCommandOperation) String() string {
|
||||||
|
if i < 0 || i >= FileCommandOperation(len(_FileCommandOperationIndex)-1) {
|
||||||
|
return fmt.Sprintf("FileCommandOperation(%d)", i)
|
||||||
|
}
|
||||||
|
return _FileCommandOperationName[_FileCommandOperationIndex[i]:_FileCommandOperationIndex[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 _FileCommandOperationNoOp() {
|
||||||
|
var x [1]struct{}
|
||||||
|
_ = x[DefaultFCO-(0)]
|
||||||
|
_ = x[FileCommandOperationCopy-(1)]
|
||||||
|
_ = x[FileCommandOperationMove-(2)]
|
||||||
|
_ = x[FileCommandOperationDelete-(3)]
|
||||||
|
_ = x[FileCommandOperationChown-(4)]
|
||||||
|
_ = x[FileCommandOperationChmod-(5)]
|
||||||
|
}
|
||||||
|
|
||||||
|
var _FileCommandOperationValues = []FileCommandOperation{DefaultFCO, FileCommandOperationCopy, FileCommandOperationMove, FileCommandOperationDelete, FileCommandOperationChown, FileCommandOperationChmod}
|
||||||
|
|
||||||
|
var _FileCommandOperationNameToValueMap = map[string]FileCommandOperation{
|
||||||
|
_FileCommandOperationName[0:0]: DefaultFCO,
|
||||||
|
_FileCommandOperationLowerName[0:0]: DefaultFCO,
|
||||||
|
_FileCommandOperationName[0:4]: FileCommandOperationCopy,
|
||||||
|
_FileCommandOperationLowerName[0:4]: FileCommandOperationCopy,
|
||||||
|
_FileCommandOperationName[4:8]: FileCommandOperationMove,
|
||||||
|
_FileCommandOperationLowerName[4:8]: FileCommandOperationMove,
|
||||||
|
_FileCommandOperationName[8:14]: FileCommandOperationDelete,
|
||||||
|
_FileCommandOperationLowerName[8:14]: FileCommandOperationDelete,
|
||||||
|
_FileCommandOperationName[14:19]: FileCommandOperationChown,
|
||||||
|
_FileCommandOperationLowerName[14:19]: FileCommandOperationChown,
|
||||||
|
_FileCommandOperationName[19:24]: FileCommandOperationChmod,
|
||||||
|
_FileCommandOperationLowerName[19:24]: FileCommandOperationChmod,
|
||||||
|
}
|
||||||
|
|
||||||
|
var _FileCommandOperationNames = []string{
|
||||||
|
_FileCommandOperationName[0:0],
|
||||||
|
_FileCommandOperationName[0:4],
|
||||||
|
_FileCommandOperationName[4:8],
|
||||||
|
_FileCommandOperationName[8:14],
|
||||||
|
_FileCommandOperationName[14:19],
|
||||||
|
_FileCommandOperationName[19:24],
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileCommandOperationString retrieves an enum value from the enum constants string name.
|
||||||
|
// Throws an error if the param is not part of the enum.
|
||||||
|
func FileCommandOperationString(s string) (FileCommandOperation, error) {
|
||||||
|
if val, ok := _FileCommandOperationNameToValueMap[s]; ok {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, ok := _FileCommandOperationNameToValueMap[strings.ToLower(s)]; ok {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("%s does not belong to FileCommandOperation values", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileCommandOperationValues returns all values of the enum
|
||||||
|
func FileCommandOperationValues() []FileCommandOperation {
|
||||||
|
return _FileCommandOperationValues
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileCommandOperationStrings returns a slice of all String values of the enum
|
||||||
|
func FileCommandOperationStrings() []string {
|
||||||
|
strs := make([]string, len(_FileCommandOperationNames))
|
||||||
|
copy(strs, _FileCommandOperationNames)
|
||||||
|
return strs
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAFileCommandOperation returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||||
|
func (i FileCommandOperation) IsAFileCommandOperation() bool {
|
||||||
|
for _, v := range _FileCommandOperationValues {
|
||||||
|
if i == v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface for FileCommandOperation
|
||||||
|
func (i FileCommandOperation) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(i.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON implements the json.Unmarshaler interface for FileCommandOperation
|
||||||
|
func (i *FileCommandOperation) UnmarshalJSON(data []byte) error {
|
||||||
|
var s string
|
||||||
|
if err := json.Unmarshal(data, &s); err != nil {
|
||||||
|
return fmt.Errorf("FileCommandOperation should be a string, got %s", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
*i, err = FileCommandOperationString(s)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText implements the encoding.TextMarshaler interface for FileCommandOperation
|
||||||
|
func (i FileCommandOperation) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(i.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText implements the encoding.TextUnmarshaler interface for FileCommandOperation
|
||||||
|
func (i *FileCommandOperation) UnmarshalText(text []byte) error {
|
||||||
|
var err error
|
||||||
|
*i, err = FileCommandOperationString(string(text))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalYAML implements a YAML Marshaler for FileCommandOperation
|
||||||
|
func (i FileCommandOperation) MarshalYAML() (interface{}, error) {
|
||||||
|
return i.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML implements a YAML Unmarshaler for FileCommandOperation
|
||||||
|
func (i *FileCommandOperation) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
var s string
|
||||||
|
if err := unmarshal(&s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
*i, err = FileCommandOperationString(s)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
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()
|
// // Add metrics for a command
|
||||||
if err != nil {
|
// commandName := "test_command"
|
||||||
t.Errorf("Failed to save metrics to file: %v", err)
|
// if _, exists := metricFile.CommandMetrics[commandName]; !exists {
|
||||||
}
|
// metricFile.CommandMetrics[commandName] = NewMetrics()
|
||||||
|
|
||||||
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
|
// // Update the metrics for the command
|
||||||
err = metricFile.SaveToFile()
|
// executionTime := 1.8 // Example execution time in seconds
|
||||||
if err != nil {
|
// success := true // Example success status
|
||||||
t.Errorf("Failed to save metrics to file: %v", err)
|
// 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)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// }
|
||||||
|
|||||||
138
pkg/backy/ssh.go
138
pkg/backy/ssh.go
@@ -5,12 +5,12 @@
|
|||||||
package backy
|
package backy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -471,17 +471,45 @@ 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)
|
var file *os.File
|
||||||
if IsCmdStdOutEnabled() {
|
if !IsHostLocal(command.Host) && command.Output.File != "" {
|
||||||
cmdOutWriters = io.MultiWriter(os.Stdout, &cmdOutBuf)
|
if filepath.IsAbs(command.Output.File) {
|
||||||
|
fileName := filepath.Base(command.Output.File)
|
||||||
|
fileName = fmt.Sprintf("%s_%s", command.RemoteHost.Host, fileName)
|
||||||
|
command.Output.File = filepath.Join(filepath.Dir(command.Output.File), fileName)
|
||||||
|
} else {
|
||||||
|
command.Output.File = fmt.Sprintf("%s_%s", command.RemoteHost.Host, command.Output.File)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdOutWriters, file, err = makeCmdOutWriters(&cmdOutBuf, command.Output.File)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error creating command output writers: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if file != nil {
|
||||||
|
file.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
// cmdOutWriters = logging.SetLoggingWriterForCommand(&cmdOutBuf, command.Output.File, IsCmdStdOutEnabled())
|
||||||
|
cmdCtxLogger = zerolog.New(cmdOutWriters).With().Timestamp().Logger()
|
||||||
|
cmdCtxLogger = command.GenerateLoggerForCmd(cmdCtxLogger)
|
||||||
|
|
||||||
|
// cmdCtxLogger.Info().Msgf("Executing %s", command.Cmd)
|
||||||
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:
|
||||||
@@ -489,17 +517,23 @@ func (command *Command) RunCmdOnHost(cmdCtxLogger zerolog.Logger, opts *ConfigOp
|
|||||||
case RemoteScriptCommandType:
|
case RemoteScriptCommandType:
|
||||||
return command.runRemoteScript(commandSession, cmdCtxLogger, &cmdOutBuf)
|
return command.runRemoteScript(commandSession, cmdCtxLogger, &cmdOutBuf)
|
||||||
case ScriptFileCommandType:
|
case ScriptFileCommandType:
|
||||||
return command.runScriptFile(commandSession, cmdCtxLogger, &cmdOutBuf)
|
commandSession.Stdout = nil
|
||||||
|
commandSession.Stderr = nil
|
||||||
|
return command.runScriptFile(commandSession, cmdCtxLogger, opts.Logger, &cmdOutBuf)
|
||||||
case PackageCommandType:
|
case PackageCommandType:
|
||||||
var remoteHostPackageExecutor RemoteHostPackageExecutor
|
var remoteHostPackageExecutor RemoteHostPackageExecutor
|
||||||
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")
|
||||||
@@ -522,6 +556,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() {
|
||||||
@@ -530,7 +565,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -606,11 +641,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)
|
||||||
@@ -651,28 +685,67 @@ func (command *Command) runScript(session *ssh.Session, cmdCtxLogger zerolog.Log
|
|||||||
}
|
}
|
||||||
|
|
||||||
// runScriptFile handles the execution of script files.
|
// runScriptFile handles the execution of script files.
|
||||||
func (command *Command) runScriptFile(session *ssh.Session, cmdCtxLogger zerolog.Logger, outputBuf *bytes.Buffer) ([]string, error) {
|
func (command *Command) runScriptFile(session *ssh.Session, cmdCtxLogger, globalLogger zerolog.Logger, outputBuf *bytes.Buffer) ([]string, error) {
|
||||||
script, err := command.prepareScriptFileBuffer()
|
script, err := command.prepareScriptFileBuffer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
session.Stdin = script
|
// session.Stdin = script
|
||||||
|
|
||||||
|
modes := ssh.TerminalModes{
|
||||||
|
ssh.ECHO: 0,
|
||||||
|
ssh.ECHOCTL: 0,
|
||||||
|
ssh.TTY_OP_ISPEED: 14400,
|
||||||
|
ssh.TTY_OP_OSPEED: 14400,
|
||||||
|
}
|
||||||
|
|
||||||
|
session.RequestPty("xterm", 80, 40, modes)
|
||||||
|
|
||||||
|
stdin, _ := session.StdinPipe()
|
||||||
|
stdout, stdOutErr := session.StdoutPipe()
|
||||||
|
if stdOutErr != nil {
|
||||||
|
return nil, fmt.Errorf("error getting stdout pipe: %w", stdOutErr)
|
||||||
|
}
|
||||||
|
|
||||||
if err := session.Shell(); err != nil {
|
if err := session.Shell(); err != nil {
|
||||||
return nil, fmt.Errorf("error starting shell: %w", err)
|
return nil, fmt.Errorf("error starting shell: %w", err)
|
||||||
}
|
}
|
||||||
|
var LogOutputToFile bool
|
||||||
if err := session.Wait(); err != nil {
|
if command.Output.File != "" || command.Output.ToLog {
|
||||||
return collectOutput(outputBuf, command.Name, cmdCtxLogger, true), fmt.Errorf("error waiting for shell: %w", err)
|
if command.Output.File != "" {
|
||||||
|
globalLogger.Info().Str("file", command.Output.File).Msg("Writing script output to file")
|
||||||
|
}
|
||||||
|
LogOutputToFile = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return collectOutput(outputBuf, command.Name, cmdCtxLogger, command.Output.ToLog), nil
|
stdin.Write(script.Bytes())
|
||||||
|
|
||||||
|
stdOutput, stdoOutReadErr := io.ReadAll(stdout)
|
||||||
|
if err := session.Wait(); err != nil {
|
||||||
|
stdOutBuff := bytes.NewBuffer(stdOutput)
|
||||||
|
// outputBuf.Write(stdOutBuff.Bytes())
|
||||||
|
// Read output
|
||||||
|
return collectOutput(stdOutBuff, command.Name, cmdCtxLogger, LogOutputToFile), fmt.Errorf("error waiting for shell: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read output
|
||||||
|
if stdoOutReadErr != nil {
|
||||||
|
return collectOutput(outputBuf, command.Name, cmdCtxLogger, LogOutputToFile), fmt.Errorf("error reading stdout after shell error: %w", stdoOutReadErr)
|
||||||
|
}
|
||||||
|
stdOutBuff := bytes.NewBuffer(stdOutput)
|
||||||
|
|
||||||
|
return collectOutput(stdOutBuff, command.Name, cmdCtxLogger, LogOutputToFile), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepareScriptBuffer prepares a buffer for inline scripts.
|
// prepareScriptBuffer prepares a buffer for inline scripts.
|
||||||
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 {
|
||||||
|
fmt.Fprintf(&buffer, "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 {
|
||||||
@@ -694,6 +767,15 @@ 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
|
||||||
|
|
||||||
|
if !command.SaveShellHistory {
|
||||||
|
buffer.WriteString("unset HISTFILE\nexport HISTSIZE=0\nexport SAVEHIST=0\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, envVar := range command.Environment {
|
||||||
|
fmt.Fprintf(&buffer, "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)
|
||||||
@@ -753,20 +835,6 @@ func readFileToBuffer(filePath string) (*bytes.Buffer, error) {
|
|||||||
return &buffer, nil
|
return &buffer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectOutput collects output from a buffer and logs it.
|
|
||||||
func collectOutput(buf *bytes.Buffer, commandName string, logger zerolog.Logger, wantOutput bool) []string {
|
|
||||||
var outputArr []string
|
|
||||||
scanner := bufio.NewScanner(buf)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
outputArr = append(outputArr, line)
|
|
||||||
if wantOutput {
|
|
||||||
logger.Info().Str("cmd", commandName).Str("output", line).Send()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return outputArr
|
|
||||||
}
|
|
||||||
|
|
||||||
// createSSHSession attempts to create a new SSH session and retries on failure.
|
// createSSHSession attempts to create a new SSH session and retries on failure.
|
||||||
func (h *Host) createSSHSession(opts *ConfigOpts) (*ssh.Session, error) {
|
func (h *Host) createSSHSession(opts *ConfigOpts) (*ssh.Session, error) {
|
||||||
session, err := h.SshClient.NewSession()
|
session, err := h.SshClient.NewSession()
|
||||||
@@ -836,7 +904,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)
|
||||||
}
|
}
|
||||||
|
|||||||
71
pkg/backy/template.go
Normal file
71
pkg/backy/template.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package backy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadVarsYAML(path string) (map[string]interface{}, error) {
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var m map[string]interface{}
|
||||||
|
if err := yaml.Unmarshal(b, &m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderTemplateFile(templatePath string, vars map[string]interface{}) ([]byte, error) {
|
||||||
|
tmplText, err := os.ReadFile(templatePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
funcs := template.FuncMap{
|
||||||
|
"env": func(k, d string) string {
|
||||||
|
if v := os.Getenv(k); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
},
|
||||||
|
"default": func(def interface{}, v interface{}) interface{} {
|
||||||
|
if v == nil {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
if s, ok := v.(string); ok && s == "" {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
},
|
||||||
|
"toYaml": func(v interface{}) string {
|
||||||
|
b, _ := yaml.Marshal(v)
|
||||||
|
return string(b)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
t := template.New(filepath.Base(templatePath)).Funcs(funcs)
|
||||||
|
t, err = t.Parse(string(tmplText))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := t.Execute(&buf, vars); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteRenderedFile(templatePath string, vars map[string]interface{}, dest string, perm os.FileMode) error {
|
||||||
|
out, err := RenderTemplateFile(templatePath, vars)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(dest, out, perm)
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package backy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"io/fs"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"strings"
|
"strings"
|
||||||
@@ -69,6 +70,7 @@ 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"`
|
||||||
|
|
||||||
@@ -78,6 +80,8 @@ type (
|
|||||||
|
|
||||||
ScriptEnvFile string `yaml:"scriptEnvFile"`
|
ScriptEnvFile string `yaml:"scriptEnvFile"`
|
||||||
|
|
||||||
|
SaveShellHistory bool `yaml:"saveShellHistory,omitempty"`
|
||||||
|
|
||||||
Output struct {
|
Output struct {
|
||||||
File string `yaml:"file,omitempty"`
|
File string `yaml:"file,omitempty"`
|
||||||
ToLog bool `yaml:"toLog,omitempty"`
|
ToLog bool `yaml:"toLog,omitempty"`
|
||||||
@@ -137,7 +141,20 @@ type (
|
|||||||
// stdin only for userOperation = password (for now)
|
// stdin only for userOperation = password (for now)
|
||||||
stdin *strings.Reader
|
stdin *strings.Reader
|
||||||
|
|
||||||
// END USER STRUCommandType FIELDS
|
// END USER CommandType FIELDS
|
||||||
|
|
||||||
|
// BEGIN FILE COMMAND FIELDS
|
||||||
|
|
||||||
|
FileOperation string `yaml:"fileOperation,omitempty"`
|
||||||
|
Source string `yaml:"source,omitempty"`
|
||||||
|
DestinationType string `yaml:"destinationType,omitempty"`
|
||||||
|
SourceType string `yaml:"sourceType,omitempty"`
|
||||||
|
Destination string `yaml:"destination,omitempty"`
|
||||||
|
Permissions fs.FileMode `yaml:"permissions,omitempty"`
|
||||||
|
Owner string `yaml:"owner,omitempty"`
|
||||||
|
Group string `yaml:"group,omitempty"`
|
||||||
|
|
||||||
|
// END FILE COMMAND FIELDS
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoteSource struct {
|
RemoteSource struct {
|
||||||
@@ -169,6 +186,12 @@ type (
|
|||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GoCronOpts struct {
|
||||||
|
BindAddress string `yaml:"bindAddress"`
|
||||||
|
UseSeconds bool `yaml:"useSeconds"`
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
}
|
||||||
|
|
||||||
ConfigOpts struct {
|
ConfigOpts struct {
|
||||||
// Cmds holds the commands for a list.
|
// Cmds holds the commands for a list.
|
||||||
// Key is the name of the command,
|
// Key is the name of the command,
|
||||||
@@ -182,6 +205,8 @@ type (
|
|||||||
// key is the host.
|
// key is the host.
|
||||||
Hosts map[string]*Host `yaml:"hosts"`
|
Hosts map[string]*Host `yaml:"hosts"`
|
||||||
|
|
||||||
|
GoCron GoCronOpts `yaml:"goCron:"`
|
||||||
|
|
||||||
Logger zerolog.Logger
|
Logger zerolog.Logger
|
||||||
|
|
||||||
// Global log level
|
// Global log level
|
||||||
@@ -303,6 +328,7 @@ type (
|
|||||||
CommandType int
|
CommandType int
|
||||||
PackageOperation int
|
PackageOperation int
|
||||||
AllowedExternalDirectives int
|
AllowedExternalDirectives int
|
||||||
|
FileCommandOperation 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
|
||||||
@@ -313,6 +339,7 @@ const (
|
|||||||
RemoteScriptCommandType // remoteScript
|
RemoteScriptCommandType // remoteScript
|
||||||
PackageCommandType // package
|
PackageCommandType // package
|
||||||
UserCommandType // user
|
UserCommandType // user
|
||||||
|
FileCommandType // file
|
||||||
)
|
)
|
||||||
|
|
||||||
//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
|
||||||
@@ -326,6 +353,16 @@ const (
|
|||||||
PackageOperationIsInstalled // isInstalled
|
PackageOperationIsInstalled // isInstalled
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:generate go run github.com/dmarkham/enumer -linecomment -yaml -text -json -type=FileCommandOperation
|
||||||
|
const (
|
||||||
|
DefaultFCO FileCommandOperation = iota //
|
||||||
|
FileCommandOperationCopy // copy
|
||||||
|
FileCommandOperationMove // move
|
||||||
|
FileCommandOperationDelete // delete
|
||||||
|
FileCommandOperationChown // chown
|
||||||
|
FileCommandOperationChmod // chmod
|
||||||
|
)
|
||||||
|
|
||||||
//go:generate go run github.com/dmarkham/enumer -linecomment -yaml -text -json -type=AllowedExternalDirectives
|
//go:generate go run github.com/dmarkham/enumer -linecomment -yaml -text -json -type=AllowedExternalDirectives
|
||||||
const (
|
const (
|
||||||
DefaultExternalDir AllowedExternalDirectives = iota
|
DefaultExternalDir AllowedExternalDirectives = iota
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
package backy
|
package backy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -22,6 +23,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 +101,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 +115,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 +173,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 strings.Builder
|
||||||
|
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 {
|
||||||
|
fmt.Fprintf(&envPrefix, "%s=%s ", key, getExternalConfigDirectiveValue(val, opts, AllowedExternalDirectiveVaultEnv))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, value := range envVars.env {
|
||||||
|
envVarArr := strings.Split(value, "=")
|
||||||
|
fmt.Fprintf(&envPrefix, "%s=%s ", envVarArr[0], getExternalConfigDirectiveValue(envVarArr[1], opts, AllowedExternalDirectiveVault))
|
||||||
|
envPrefix.WriteString("\n")
|
||||||
|
}
|
||||||
|
return envPrefix.String() + 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 {
|
||||||
@@ -190,6 +221,47 @@ func CheckConfigValues(config *koanf.Koanf, file string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// collectOutput collects output from a buffer and logs it.
|
||||||
|
func collectOutput(buf *bytes.Buffer, commandName string, logger zerolog.Logger, wantOutput bool) []string {
|
||||||
|
var outputArr []string
|
||||||
|
scanner := bufio.NewScanner(buf)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
clean := sanitizeString(line)
|
||||||
|
outputArr = append(outputArr, clean)
|
||||||
|
if wantOutput {
|
||||||
|
logger.Info().Str("cmd", commandName).Str("output", clean).Send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return outputArr
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanitizeString removes ANSI escape sequences and non-printable control characters
|
||||||
|
// while preserving tabs. This helps remove color codes and other terminal control
|
||||||
|
// characters from remote command output.
|
||||||
|
func sanitizeString(s string) string {
|
||||||
|
// Remove common ANSI CSI sequences like "\x1b[31m" etc.
|
||||||
|
var ansiCSI = regexp.MustCompile(`\x1b\[[0-9;?]*[ -/]*[@-~]`)
|
||||||
|
s = ansiCSI.ReplaceAllString(s, "")
|
||||||
|
|
||||||
|
// Remove OSC sequences started by ESC ] and terminated by BEL or ESC\
|
||||||
|
var osc = regexp.MustCompile(`(?s)"].*?(?:|\\)`)
|
||||||
|
s = osc.ReplaceAllString(s, "")
|
||||||
|
|
||||||
|
// Sometimes the ESC has been stripped earlier and we are left with sequences like "]2;title]1;"
|
||||||
|
// Remove leftover bracketed sequences like "]<digits>;<text>"
|
||||||
|
var leftoverOSC = regexp.MustCompile(`\][0-9]+[^\]]*`)
|
||||||
|
s = leftoverOSC.ReplaceAllString(s, "")
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
for _, r := range s {
|
||||||
|
if r == '\t' || (r >= 0x20 && r != 0x7f) {
|
||||||
|
b.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
func testFile(c string) error {
|
func testFile(c string) error {
|
||||||
if strings.TrimSpace(c) != "" {
|
if strings.TrimSpace(c) != "" {
|
||||||
file, fileOpenErr := os.Open(c)
|
file, fileOpenErr := os.Open(c)
|
||||||
@@ -403,24 +475,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 +510,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)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package logging
|
package logging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -66,6 +67,48 @@ func SetLoggingWriters(logFile string) (writers zerolog.LevelWriter) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetLoggingWriterForCommand(buf *bytes.Buffer, logFile string, logToConsole bool) (writers zerolog.LevelWriter) {
|
||||||
|
|
||||||
|
console := zerolog.ConsoleWriter{}
|
||||||
|
if logToConsole {
|
||||||
|
|
||||||
|
console = zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC1123}
|
||||||
|
console.FormatLevel = func(i interface{}) string {
|
||||||
|
return strings.ToUpper(fmt.Sprintf("| %-6s|", i))
|
||||||
|
}
|
||||||
|
console.FormatMessage = func(i any) string {
|
||||||
|
if i == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("MSG: %s", i)
|
||||||
|
}
|
||||||
|
console.FormatFieldName = func(i interface{}) string {
|
||||||
|
return fmt.Sprintf("%s: ", i)
|
||||||
|
}
|
||||||
|
console.FormatFieldValue = func(i interface{}) string {
|
||||||
|
return fmt.Sprintf("%s", i)
|
||||||
|
// return strings.ToUpper(fmt.Sprintf("%s", i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileLogger := &lumberjack.Logger{
|
||||||
|
MaxSize: 50, // megabytes
|
||||||
|
MaxBackups: 3,
|
||||||
|
MaxAge: 28, //days
|
||||||
|
Compress: true, // disabled by default
|
||||||
|
}
|
||||||
|
fileLogger.Filename = logFile
|
||||||
|
// UNIX Time is faster and smaller than most timestamps
|
||||||
|
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||||
|
// zerolog.TimeFieldFormat = time.RFC1123
|
||||||
|
writers = zerolog.MultiLevelWriter(fileLogger)
|
||||||
|
|
||||||
|
if logToConsole {
|
||||||
|
writers = zerolog.MultiLevelWriter(console, fileLogger)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func IsConsoleLoggingEnabled() bool {
|
func IsConsoleLoggingEnabled() bool {
|
||||||
return os.Getenv("BACKY_CONSOLE_LOGGING") == "enabled"
|
return os.Getenv("BACKY_CONSOLE_LOGGING") == "enabled"
|
||||||
}
|
}
|
||||||
|
|||||||
12
tests/FileOps.yml
Normal file
12
tests/FileOps.yml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
commands:
|
||||||
|
copyFile:
|
||||||
|
type: file
|
||||||
|
fileOperation: copy
|
||||||
|
source: /home/andrew/Projects/backy/tests/data/fileops/source.txt
|
||||||
|
destination: /home/andrew/Projects/backy/tests/data/fileops/destination.txt
|
||||||
|
copyRemoteFile:
|
||||||
|
type: file
|
||||||
|
fileOperation: copy
|
||||||
|
sourceType: rempte
|
||||||
|
source: ssh://backy@localhost:2222/home/backy/remote_source.txt
|
||||||
|
destination: /home/andrew/Projects/backy/tests/data/fileops/remote_destination.txt
|
||||||
@@ -7,10 +7,11 @@ commands:
|
|||||||
success:
|
success:
|
||||||
- successCmd
|
- successCmd
|
||||||
|
|
||||||
errorCmd:
|
successCmd:
|
||||||
name: get docker version
|
name: get docker version
|
||||||
cmd: docker
|
cmd: docker
|
||||||
getOutput: true
|
output:
|
||||||
outputToLog: true
|
file: docker_version_success.txt
|
||||||
|
toLog: true
|
||||||
Args:
|
Args:
|
||||||
- "-v"
|
- "-v"
|
||||||
1
tests/data/fileops/destination.txt
Normal file
1
tests/data/fileops/destination.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
This is some test data.
|
||||||
1
tests/data/fileops/source.txt
Normal file
1
tests/data/fileops/source.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
This is some test data.
|
||||||
39
tests/docker/README.md
Normal file
39
tests/docker/README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
SSH test container
|
||||||
|
==================
|
||||||
|
|
||||||
|
This folder contains a simple Docker-based SSH server used for integration tests.
|
||||||
|
|
||||||
|
Quick start
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Start the container (builds image if needed):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./start.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Stop the container:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./stop.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Access
|
||||||
|
------
|
||||||
|
|
||||||
|
- SSH endpoint: `localhost:2222`
|
||||||
|
- Test user: `backy` with password `backy` (password auth enabled)
|
||||||
|
- Root user: `root` with password `test`
|
||||||
|
- Public key `backytest.pub` is installed for both `backy` and `root`
|
||||||
|
|
||||||
|
Running tests
|
||||||
|
-------------
|
||||||
|
|
||||||
|
1. Start the container (`./start.sh`).
|
||||||
|
2. From the repo root, run your tests (example):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GO_TEST_SSH_ADDR=localhost:2222 go test ./... -v
|
||||||
|
```
|
||||||
|
|
||||||
|
If your tests rely on an SSH private key, use `tests/docker/backytest` as the private key and restrict access appropriately.
|
||||||
@@ -1,4 +1,11 @@
|
|||||||
cd ~/Projects/backy/tests/docker
|
#!/usr/bin/env bash
|
||||||
docker container rm -f ssh_server_container
|
set -euo pipefail
|
||||||
docker build -t ssh_server_image .
|
|
||||||
|
# Build and run the test SSH container from the tests/docker directory
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
docker container rm -f ssh_server_container 2>/dev/null || true
|
||||||
|
docker build -t ssh_server_image "$SCRIPT_DIR"
|
||||||
docker run -d -p 2222:22 --name ssh_server_container ssh_server_image
|
docker run -d -p 2222:22 --name ssh_server_container ssh_server_image
|
||||||
|
sleep 5
|
||||||
|
ssh-keyscan -p 2222 localhost > $SCRIPT_DIR/known_hosts
|
||||||
8
tests/docker/compose.yml
Normal file
8
tests/docker/compose.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
services:
|
||||||
|
ssh_server:
|
||||||
|
build: .
|
||||||
|
image: backy_ssh_server:latest
|
||||||
|
container_name: backy_ssh_server
|
||||||
|
ports:
|
||||||
|
- "2222:22"
|
||||||
|
restart: "no"
|
||||||
8
tests/docker/known_hosts
Normal file
8
tests/docker/known_hosts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# localhost:2222 SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.14
|
||||||
|
# localhost:2222 SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.14
|
||||||
|
[localhost]:2222 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDATufWA1HRnNayIQLjSpA2+P9N6h0WF+jP+abMaINlZkiHFnFVDAoqD5/onVXymskrgQaKEYmBOs+Kv0t+Acvdor2IcvYgFueSm+jkslpSK/uuf1mx0gVJO77S2BIjqyWtUzVv96Iy4Gjt2RsrnalgYNYmi3OyPkG0IUA+3Im+2gztSECCy+nW3R/vaoPLwr4kImpLlrijcSHc4mHOY6BurrcWKNuGrsvTAOKgUZqlya6uDd+yD7fUfsmL1MqBKwZqfP3JAdp/Dd+laNNGcvEM4WhzYFSPfhqblewD0rjbto9MSOSXLyQz5RPmdITj/m5M4lj2ECmcI2gzraDMoj8ZkuJAss50oX6fmVUZestN5jlz7Y7XKEvXuH8qfLHKwaOUTZlcGbfAMz6uSrh8DNT6KzRG4j5nZ9Z5pTn1huz/p6jnJUGuHt2Ez3EK+isM+sHS6TntXavIkebaq7ErcBCO8A1fZFZlhlHoI9o9W62tMY7gbtlGodW8dKxK89+1a88=
|
||||||
|
# localhost:2222 SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.14
|
||||||
|
[localhost]:2222 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBK9fEYfiGGgu0Eh7X2JT4jR4+utcfpm6Ee+Cer1x/XbMHzCPZg6YmYy6OaCSms/0VJ/QWxD+0HlsO7sqO5oeO60=
|
||||||
|
# localhost:2222 SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.14
|
||||||
|
[localhost]:2222 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKT5+Cbi/ynOAPzwv0IaOVBtGFYtW33LIvNUuBKYqqyJ
|
||||||
|
# localhost:2222 SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.14
|
||||||
7
tests/docker/start.sh
Executable file
7
tests/docker/start.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
echo "Starting SSH test container (building if needed)..."
|
||||||
|
docker compose -f "$DIR/compose.yml" up -d --build
|
||||||
|
echo "Container started on localhost:2222"
|
||||||
7
tests/docker/stop.sh
Executable file
7
tests/docker/stop.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
echo "Stopping and removing SSH test container..."
|
||||||
|
docker compose -f "$DIR/compose.yml" down --remove-orphans
|
||||||
|
echo "Stopped."
|
||||||
5
tests/example.tmpl
Normal file
5
tests/example.tmpl
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{{ .greeting }}, {{ .name }}!
|
||||||
|
port: {{ .port | default "8080" }}
|
||||||
|
envHOME: {{ env "HOME" "" }}
|
||||||
|
debugYaml:
|
||||||
|
{{ toYaml . }}
|
||||||
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
81
tests/integration_test.go
Normal file
81
tests/integration_test.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{"--config", configFile}
|
||||||
|
hosts := []string{"localhost"}
|
||||||
|
|
||||||
|
execListArgs := setupExecListRunOnHosts([]string{"echoTestSuccess"}, hosts, args)
|
||||||
|
|
||||||
|
cmd := exec.Command("go", execListArgs...)
|
||||||
|
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:\n\n%s\n\n", string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupExecListRunOnHosts(cmds, hosts, args []string) []string {
|
||||||
|
baseArgs := []string{"run", "../backy.go", "exec", "host"}
|
||||||
|
for _, h := range hosts {
|
||||||
|
hostArg := "-m"
|
||||||
|
hostArg += h
|
||||||
|
baseArgs = append(baseArgs, hostArg)
|
||||||
|
}
|
||||||
|
for _, c := range cmds {
|
||||||
|
cmdArg := "-c"
|
||||||
|
cmdArg += c
|
||||||
|
baseArgs = append(baseArgs, cmdArg)
|
||||||
|
}
|
||||||
|
return append(baseArgs, args...)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
3
tests/vars.yaml
Normal file
3
tests/vars.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
name: Alice
|
||||||
|
greeting: Hello
|
||||||
|
port: 9090
|
||||||
Reference in New Issue
Block a user