Compare commits

..

No commits in common. "develop" and "v0.6.0" have entirely different histories.

46 changed files with 648 additions and 1389 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

2
.gitignore vendored
View File

@ -1,4 +1,2 @@
dist/
.codegpt

View File

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

View File

@ -1,12 +1,15 @@
steps:
build:
image: hugomods/hugo:ci
image: klakegg/hugo:ext-debian-ci
commands:
- git submodule foreach 'git fetch origin; git checkout $(git describe --tags `git rev-list --tags --max-count=1`);'
- cd docs
- hugo mod get -u github.com/divinerites/plausible-hugo
- hugo mod get -u github.com/McShelby/hugo-theme-relearn@7.3.1
- hugo mod get -u ./...
- hugo
when:
- event: push
branch: master
path: "docs/*"
deploy:
image: codingkoopa/git-rsync-openssh
@ -26,4 +29,6 @@ steps:
secrets: [ ssh_host_key, ssh_deploy_key, ssh_passphrase ]
when:
- branch: master
- event: push
branch: master
path: "docs/*"

View File

@ -6,15 +6,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).
## v0.6.1 - 2025-01-04
### Fixed
* When running a list, hooks now run explicitly after the command executes. Fixed panic due to improper logic.
## v0.6.0 - 2025-01-04
### Added
* Command Type Package - allows one to perform package operations [docs](https://backy.cybershell.xyz/config/packages/)
* Exec subcommand `host` allows for parallel execution of commands on hosts. [See docs](https://backy.cybershell.xyz/cli/exec)
## v0.5.0 - 2024-11-19
### Added
* Lists can now go in a file. See docs for more information.

View File

@ -31,7 +31,8 @@ func init() {
func Backup(cmd *cobra.Command, args []string) {
backyConfOpts := backy.NewOpts(cfgFile, backy.AddCommandLists(cmdLists))
backyConfOpts.InitConfig()
backyConfOpts.ReadConfig()
backy.ReadConfig(backyConfOpts)
backyConfOpts.RunListConfig("")
for _, host := range backyConfOpts.Hosts {

View File

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

View File

@ -33,8 +33,8 @@ func execute(cmd *cobra.Command, args []string) {
logging.ExitWithMSG("Please provide a command to run. Pass --help to see options.", 1, nil)
}
opts := backy.NewOpts(cfgFile, backy.AddCommands(args), backy.SetLogFile(logFile))
opts := backy.NewOpts(cfgFile, backy.AddCommands(args))
opts.InitConfig()
opts.ReadConfig()
opts.ExecuteCmds()
// opts.InitMongo()
backy.ReadConfig(opts).ExecuteCmds(opts)
}

View File

@ -29,10 +29,10 @@ func init() {
// 2. stdin (on command line) (TODO)
func Host(cmd *cobra.Command, args []string) {
backyConfOpts := backy.NewOpts(cfgFile, backy.SetLogFile(logFile))
backyConfOpts := backy.NewOpts(cfgFile)
backyConfOpts.InitConfig()
backyConfOpts.ReadConfig()
backy.ReadConfig(backyConfOpts)
// check CLI input
if hostsList == nil {

View File

@ -31,7 +31,7 @@ func init() {
func List(cmd *cobra.Command, args []string) {
// setup based on whats passed in:
// settup based on whats passed in:
// - cmds
// - lists
// - if none, list all commands
@ -42,7 +42,8 @@ func List(cmd *cobra.Command, args []string) {
opts := backy.NewOpts(cfgFile)
opts.InitConfig()
opts.ReadConfig()
opts = backy.ReadConfig(opts)
opts.ListCommand("rm-sn-db")
}

View File

@ -15,7 +15,6 @@ var (
// Used for flags.
cfgFile string
verbose bool
logFile string
rootCmd = &cobra.Command{
Use: "backy",
@ -34,8 +33,6 @@ func Execute() {
func init() {
rootCmd.PersistentFlags().StringVar(&logFile, "log-file", "", "log file to write to")
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "f", "", "config file to read from")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Sets verbose level")

View File

@ -7,7 +7,7 @@ import (
"github.com/spf13/cobra"
)
const versionStr = "0.6.1"
const versionStr = "0.6.0"
var (
versionCmd = &cobra.Command{

137
docs/config.toml Normal file
View File

@ -0,0 +1,137 @@
baseURL = 'http://backy.cybershell.xyz/'
languageCode = 'en-us'
title = 'A tool for commands'
# Change the default theme to be use when building the site with Hugo
theme = [ "hugo-theme-relearn", "plausible-hugo"] # Add this theme to your already existing other themes
# For search functionality
[outputs]
home = [ "HTML", "RSS", "PRINT"]
[module]
[[module.imports]]
path = "github.com/divinerites/plausible-hugo"
[params]
# This controls whether submenus will be expanded (true), or collapsed (false) in the
# menu; if no setting is given, the first menu level is set to false, all others to true;
# this can be overridden in the pages frontmatter
alwaysopen = false
# Prefix URL to edit current page. Will display an "Edit" button on top right hand corner of every page.
# Useful to give opportunity to people to create merge request for your doc.
# See the config.toml file from this documentation site to have an example.
editURL = ""
# Description of the site, will be used in meta information
description = ""
# Shows a checkmark for visited pages on the menu
showVisitedLinks = false
# Disable search function. It will hide search bar
disableSearch = false
# Disable search in hidden pages, otherwise they will be shown in search box
disableSearchHiddenPages = false
# Disables hidden pages from showing up in the sitemap and on Google (et all), otherwise they may be indexed by search engines
disableSeoHiddenPages = false
# Disables hidden pages from showing up on the tags page although the tag term will be displayed even if all pages are hidden
disableTagHiddenPages = false
# Javascript and CSS cache are automatically busted when new version of site is generated.
# Set this to true to disable this behavior (some proxies don't handle well this optimization)
disableAssetsBusting = false
# Set this to true if you want to disable generation for generator version meta tags of hugo and the theme;
# don't forget to also set Hugo's disableHugoGeneratorInject=true, otherwise it will generate a meta tag into your home page
disableGeneratorVersion = false
# Set this to true to disable copy-to-clipboard button for inline code.
disableInlineCopyToClipBoard = false
# A title for shortcuts in menu is set by default. Set this to true to disable it.
disableShortcutsTitle = false
# If set to false, a Home button will appear below the search bar on the menu.
# It is redirecting to the landing page of the current language if specified. (Default is "/")
disableLandingPageButton = true
# When using mulitlingual website, disable the switch language button.
disableLanguageSwitchingButton = false
# Hide breadcrumbs in the header and only show the current page title
disableBreadcrumb = true
# If set to true, hide table of contents menu in the header of all pages
disableToc = false
# If set to false, load the MathJax module on every page regardless if a MathJax shortcode is present
math = true
# Specifies the remote location of the MathJax js
customMathJaxURL = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"
# Initialization parameter for MathJax, see MathJax documentation
mathJaxInitialize = "{}"
# If set to false, load the Mermaid module on every page regardless if a Mermaid shortcode or Mermaid codefence is present
mermaid = true
# Specifies the remote location of the Mermaid js
customMermaidURL = "https://unpkg.com/mermaid/dist/mermaid.min.js"
# Initialization parameter for Mermaid, see Mermaid documentation
mermaidInitialize = "{ \"theme\": \"default\" }"
# If set to false, load the Swagger module on every page regardless if a Swagger shortcode is present
disableSwagger = false
# Specifies the remote location of the RapiDoc js
customSwaggerURL = "https://unpkg.com/rapidoc/dist/rapidoc-min.js"
# Initialization parameter for Swagger, see RapiDoc documentation
swaggerInitialize = "{ \"theme\": \"light\" }"
# Hide Next and Previous page buttons normally displayed full height beside content
disableNextPrev = true
# Order sections in menu by "weight" or "title". Default to "weight";
# this can be overridden in the pages frontmatter
ordersectionsby = "weight"
[[params.themeVariant]]
auto = []
identifier = 'relearn-auto'
name = 'Relearn Light/Dark'
[[params.themeVariant]]
identifier = 'relearn-light'
[[params.themeVariant]]
identifier = 'relearn-dark'
[[params.themeVariant]]
identifier = 'relearn-bright'
[[params.themeVariant]]
auto = ['zen-light', 'zen-dark']
identifier = 'zen-auto'
name = 'Zen Light/Dark'
[[params.themeVariant]]
identifier = 'zen-light'
[[params.themeVariant]]
identifier = 'zen-dark'
[[params.themeVariant]]
auto = ['learn', 'neon']
identifier = 'retro-auto'
name = 'Retro Learn/Neon'
[[params.themeVariant]]
identifier = 'neon'
[[params.themeVariant]]
identifier = 'learn'
# Change the title separator. Default to "::".
titleSeparator = "-"
# If set to true, the menu in the sidebar will be displayed in a collapsible tree view. Although the functionality works with old browsers (IE11), the display of the expander icons is limited to modern browsers
collapsibleMenu = true
# If a single page can contain content in multiple languages, add those here
additionalContentLanguage = [ "en" ]
# If set to true, no index.html will be appended to prettyURLs; this will cause pages not
# to be servable from the file system
disableExplicitIndexURLs = false
# For external links you can define how they are opened in your browser; this setting will only be applied to the content area but not the shortcut menu
externalLinkTarget = "_blank"
# Author of the site, will be used in meta information
[params.author]
name = "Andrew Woodlee"
[params.plausible]
enable = true # Whether to enable plausible tracking
domain = "backy.cybershell.xyz" # Plausible "domain" name/id in your dashboard
outbound_link = true
gitstar = false
selfhosted_domain = "stats.andrewnw.com" # Self-hosted plausible domain

View File

@ -1,78 +0,0 @@
baseURL: https://backy.cybershell.xyz/
languageCode: en-us
title: A tool for commands
theme:
- hugo-theme-relearn
- plausible-hugo
outputs:
home:
- HTML
- RSS
- PRINT
module:
imports:
- path: github.com/divinerites/plausible-hugo
- path: github.com/McShelby/hugo-theme-relearn
params:
themeVariant:
- auto: []
identifier: relearn-auto
name: Relearn Light/Dark
- identifier: relearn-light
- identifier: relearn-dark
- identifier: relearn-bright
- auto:
- zen-light
- zen-dark
identifier: zen-auto
name: Zen Light/Dark
- identifier: zen-light
- identifier: zen-dark
- auto:
- learn
- neon
identifier: retro-auto
name: Retro Learn/Neon
- identifier: neon
- identifier: learn
plausible:
enable: true
domain: backy.cybershell.xyz
outbound_link: true
gitstar: false
selfhosted_domain: stats.andrewnw.com
author:
name: Andrew Woodlee
alwaysopen: false
editURL: ""
description: ""
showVisitedLinks: false
disableSearch: false
disableSearchHiddenPages: false
disableSeoHiddenPages: false
disableTagHiddenPages: false
disableAssetsBusting: false
disableGeneratorVersion: false
disableInlineCopyToClipBoard: false
disableShortcutsTitle: false
disableLandingPageButton: true
disableLanguageSwitchingButton: false
disableBreadcrumb: true
disableToc: false
math: true
customMathJaxURL: https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js
mathJaxInitialize: "{}"
mermaid: true
customMermaidURL: https://unpkg.com/mermaid/dist/mermaid.min.js
mermaidInitialize: '{ "theme": "default" }'
disableSwagger: false
customSwaggerURL: https://unpkg.com/rapidoc/dist/rapidoc-min.js
swaggerInitialize: '{ "theme": "light" }'
disableNextPrev: true
ordersectionsby: weight
titleSeparator: "-"
collapsibleMenu: true
additionalContentLanguage:
- en
disableExplicitIndexURLs: false
externalLinkTarget: _blank

View File

@ -17,10 +17,6 @@ Feel free to open a [PR](https://git.andrewnw.xyz/CyberShell/backy/pulls), raise
- Allows easy configuration of executable commands
- Allows for running package operations
- Allows configuring failure, success, and final hooks
- Allows for commands to be run on many hosts over SSH
- Commands can be grouped in list to run in specific order

View File

@ -3,6 +3,6 @@ module git.andrewnw.xyz/CyberShell/backy/docs
go 1.19
require (
github.com/McShelby/hugo-theme-relearn v0.0.0-20250103114405-80e448e5bdaa // indirect
github.com/McShelby/hugo-theme-relearn v0.0.0-20230209073138-890d12ea922d // indirect
github.com/divinerites/plausible-hugo v1.21.1 // indirect
)

View File

@ -1,10 +1,4 @@
github.com/McShelby/hugo-theme-relearn v0.0.0-20230209073138-890d12ea922d h1:weq1mrQ/qNAvGrNgvZVL1K8adbT3bswZf2ABLr/LCIA=
github.com/McShelby/hugo-theme-relearn v0.0.0-20230209073138-890d12ea922d/go.mod h1:mKQQdxZNIlLvAj8X3tMq+RzntIJSr9z7XdzuMomt0IM=
github.com/McShelby/hugo-theme-relearn v0.0.0-20241210183303-16d4de84becf h1:bMx4kwM7Q+dAzvSOWs3XWZ25o+n4mI0GPHqzbzeWb3M=
github.com/McShelby/hugo-theme-relearn v0.0.0-20241210183303-16d4de84becf/go.mod h1:mKQQdxZNIlLvAj8X3tMq+RzntIJSr9z7XdzuMomt0IM=
github.com/McShelby/hugo-theme-relearn v0.0.0-20250102210630-dd0597ffa4b2 h1:sWaC1/dL65v3iRvblEAaBLpKC5TIT0R9JASk1hZNET8=
github.com/McShelby/hugo-theme-relearn v0.0.0-20250102210630-dd0597ffa4b2/go.mod h1:mKQQdxZNIlLvAj8X3tMq+RzntIJSr9z7XdzuMomt0IM=
github.com/McShelby/hugo-theme-relearn v0.0.0-20250103114405-80e448e5bdaa h1:G+OnMEzK4XOzbbcf1SmaGyOYJ0h5idp/IJdguWs8ioU=
github.com/McShelby/hugo-theme-relearn v0.0.0-20250103114405-80e448e5bdaa/go.mod h1:mKQQdxZNIlLvAj8X3tMq+RzntIJSr9z7XdzuMomt0IM=
github.com/divinerites/plausible-hugo v1.21.1 h1:ZTWwjhZ0PmLMacCVGlcGiYFEZW7VaYE767tchDskOug=
github.com/divinerites/plausible-hugo v1.21.1/go.mod h1:cxr+YB3FUwbLon8KCs4pV4Ankbkq6lJxTQUpNb5KqPo=

@ -1 +0,0 @@
Subproject commit 80e448e5bdaa92c87ee0d0d86f1125c8606ebf5f

View File

@ -28,12 +28,9 @@ commands:
cmd: hostname
update-docker:
type: package
# shell: zsh
packageName: docker-ce
Args:
- docker-ce-cli
packageManager: apt
packageOperation: upgrade
packageName: docker-ce
packageVersion: "5:27.4.1-1~debian.12~bookworm"
cmd-lists:
cmds-to-run: # this can be any name you want
@ -70,7 +67,7 @@ hosts:
# optional
logging:
verbose: true
file: ./backy.log
file: /path/to/logs/commands.log
console: false
cmd-std-out: false

47
go.mod
View File

@ -1,53 +1,32 @@
module git.andrewnw.xyz/CyberShell/backy
go 1.21
toolchain go1.22.2
go 1.20
replace git.andrewnw.xyz/CyberShell/backy => /home/andrew/Projects/backy
require (
github.com/aws/aws-sdk-go-v2/config v1.18.27
github.com/aws/aws-sdk-go-v2/service/s3 v1.72.0
github.com/go-co-op/gocron v1.33.1
github.com/hashicorp/vault/api v1.10.0
github.com/joho/godotenv v1.5.1
github.com/kevinburke/ssh_config v1.2.0
github.com/knadh/koanf/parsers/yaml v0.1.0
github.com/knadh/koanf/providers/rawbytes v0.1.0
github.com/knadh/koanf/providers/file v0.1.0
github.com/knadh/koanf/v2 v2.0.1
github.com/mattn/go-isatty v0.0.19
github.com/nikoksr/notify v0.41.0
github.com/pkg/errors v0.9.1
github.com/rs/zerolog v1.30.0
github.com/sethvargo/go-password v0.3.1
github.com/spf13/cobra v1.7.0
golang.org/x/crypto v0.31.0
golang.org/x/crypto v0.13.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mautrix v0.16.0
mvdan.cc/sh/v3 v3.7.0
)
require (
github.com/aws/aws-sdk-go-v2 v1.32.7 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.26 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.26 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.12 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 // indirect
github.com/aws/smithy-go v1.22.1 // indirect
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
@ -60,7 +39,6 @@ require (
github.com/hashicorp/go-sockaddr v1.0.5 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible // indirect
github.com/knadh/koanf/maps v0.1.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
@ -70,23 +48,22 @@ require (
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/stretchr/objx v0.5.1 // indirect
github.com/stretchr/testify v1.8.4 // indirect
github.com/tidwall/gjson v1.16.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
go.mau.fi/util v0.0.0-20230906155759-14bad39a8718 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.3.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
maunium.net/go/maulogger/v2 v2.4.1 // indirect
)

103
go.sum
View File

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

View File

@ -45,19 +45,11 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
}
)
// Get the command type
// This must be done before concatenating the arguments
command = getCommandType(command)
for _, v := range command.Args {
ArgsStr += fmt.Sprintf(" %s", v)
}
if command.Type == "user" {
if command.UserOperation == "password" {
cmdCtxLogger.Info().Str("password", command.UserPassword).Msg("user password to be updated")
}
}
command = getPackageCommand(command)
var errSSH error
// is host defined
@ -68,39 +60,52 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
}
} else {
// Handle package operations
if command.Type == "package" && command.PackageOperation == "checkVersion" {
cmdCtxLogger.Info().Str("package", command.PackageName).Msg("Checking package versions")
// Execute the package version command
cmd := exec.Command(command.Cmd, command.Args...)
cmdOutWriters = io.MultiWriter(&cmdOutBuf)
cmd.Stdout = cmdOutWriters
cmd.Stderr = cmdOutWriters
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("error running command %s: %w", ArgsStr, err)
}
return parsePackageVersion(cmdOutBuf.String(), cmdCtxLogger, command, cmdOutBuf)
}
var localCMD *exec.Cmd
var err error
if command.Shell != "" {
cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s on local machine in %s", command.Name, command.Shell)).Send()
ArgsStr = fmt.Sprintf("%s %s", command.Cmd, ArgsStr)
localCMD = exec.Command(command.Shell, "-c", ArgsStr)
localCMD := exec.Command(command.Shell, "-c", ArgsStr)
} else {
if command.Dir != nil {
localCMD.Dir = *command.Dir
}
injectEnvIntoLocalCMD(envVars, localCMD, cmdCtxLogger)
cmdOutWriters = io.MultiWriter(&cmdOutBuf)
if IsCmdStdOutEnabled() {
cmdOutWriters = io.MultiWriter(os.Stdout, &cmdOutBuf)
}
localCMD.Stdout = cmdOutWriters
localCMD.Stderr = cmdOutWriters
err = localCMD.Run()
outScanner := bufio.NewScanner(&cmdOutBuf)
for outScanner.Scan() {
outMap := make(map[string]interface{})
outMap["cmd"] = command.Name
outMap["output"] = outScanner.Text()
if str, ok := outMap["output"].(string); ok {
outputArr = append(outputArr, str)
}
cmdCtxLogger.Info().Fields(outMap).Send()
}
if err != nil {
cmdCtxLogger.Error().Err(fmt.Errorf("error when running cmd %s: %w", command.Name, err)).Send()
return outputArr, err
}
return outputArr, nil
}
cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s on local machine", command.Name)).Send()
localCMD = exec.Command(command.Cmd, command.Args...)
}
localCMD := exec.Command(command.Cmd, command.Args...)
if command.Dir != nil {
localCMD.Dir = *command.Dir
@ -129,9 +134,7 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
if str, ok := outMap["output"].(string); ok {
outputArr = append(outputArr, str)
}
// if command.GetOutput {
cmdCtxLogger.Info().Fields(outMap).Send()
// }
}
if err != nil {
cmdCtxLogger.Error().Err(fmt.Errorf("error when running cmd %s: %w", command.Name, err)).Send()
@ -141,123 +144,115 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
return outputArr, nil
}
func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<- CmdResult, opts *ConfigOpts) {
// cmdListWorker
func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<- string, opts *ConfigOpts) {
// iterate over list to run
res := CmdListResults{}
for list := range jobs {
fieldsMap := map[string]interface{}{"list": list.Name}
fieldsMap := make(map[string]interface{})
fieldsMap["list"] = list.Name
var cmdLogger zerolog.Logger
var cmdsRan []string
var outStructArr []outStruct
var hasError bool // Tracks if any command in the list failed
var count int // count of how many commands have been executed
var cmdsRan []string // store the commands that have been executed
var outStructArr []outStruct // stores output messages
for _, cmd := range list.Order {
currentCmd := opts.Cmds[cmd].Name
fieldsMap["cmd"] = opts.Cmds[cmd].Name
cmdToRun := opts.Cmds[cmd]
currentCmd := cmdToRun.Name
fieldsMap["cmd"] = currentCmd
cmdLogger = cmdToRun.GenerateLogger(opts)
cmdLogger.Info().Fields(fieldsMap).Send()
outputArr, runErr := cmdToRun.RunCmd(cmdLogger, opts)
outputArr, runOutErr := cmdToRun.RunCmd(cmdLogger, opts)
if list.NotifyConfig != nil {
// check if the command output should be included
if cmdToRun.GetOutput || list.GetOutput {
outputStruct := outStruct{
CmdName: cmdToRun.Name,
CmdExecuted: currentCmd,
Output: outputArr,
}
outStructArr = append(outStructArr, outputStruct)
}
}
count++
if runOutErr != nil {
res.ErrCmd = cmd
if list.NotifyConfig != nil {
var errMsg bytes.Buffer
errStruct := make(map[string]interface{})
errStruct["listName"] = list.Name
errStruct["Command"] = currentCmd
errStruct["Cmd"] = cmd
errStruct["Args"] = opts.Cmds[cmd].Args
errStruct["Err"] = runOutErr
errStruct["CmdsRan"] = cmdsRan
errStruct["Output"] = outputArr
errStruct["CmdOutput"] = outStructArr
tmpErr := msgTemps.err.Execute(&errMsg, errStruct)
if tmpErr != nil {
cmdLogger.Err(tmpErr).Send()
}
notifySendErr := list.NotifyConfig.Send(context.Background(), fmt.Sprintf("List %s failed", list.Name), errMsg.String())
if notifySendErr != nil {
cmdLogger.Err(notifySendErr).Send()
}
}
cmdLogger.Err(runOutErr).Send()
break
} else {
cmdsRan = append(cmdsRan, cmd)
if runErr != nil {
if count == len(list.Order) {
var successMsg bytes.Buffer
// Log the error and send a failed result
cmdLogger.Err(runErr).Send()
results <- CmdResult{CmdName: cmd, ListName: list.Name, Error: runErr}
// if notification config is not nil, and NotifyOnSuccess is true or GetOuput is true,
// then send notification
if list.NotifyConfig != nil && (list.NotifyOnSuccess || list.GetOutput) {
successStruct := make(map[string]interface{})
// Execute error hooks for the failed command
cmdToRun.ExecuteHooks("error", opts)
successStruct["listName"] = list.Name
successStruct["CmdsRan"] = cmdsRan
// Notify failure
if list.NotifyConfig != nil {
notifyError(cmdLogger, msgTemps, list, cmdsRan, outStructArr, runErr, cmdToRun)
}
hasError = true
successStruct["CmdOutput"] = outStructArr
tmpErr := msgTemps.success.Execute(&successMsg, successStruct)
if tmpErr != nil {
cmdLogger.Err(tmpErr).Send()
break
}
// Collect output if required
if list.GetOutput || cmdToRun.GetOutput {
outStructArr = append(outStructArr, outStruct{
CmdName: currentCmd,
CmdExecuted: currentCmd,
Output: outputArr,
})
err := list.NotifyConfig.Send(context.Background(), fmt.Sprintf("List %s succeeded", list.Name), successMsg.String())
if err != nil {
cmdLogger.Err(err).Send()
}
}
// Notify success if no errors occurred
if !hasError && list.NotifyConfig != nil && (list.NotifyOnSuccess || list.GetOutput) {
notifySuccess(cmdLogger, msgTemps, list, cmdsRan, outStructArr)
}
// Execute success and final hooks for all commands
for _, cmd := range list.Order {
cmdToRun := opts.Cmds[cmd]
// Execute success hooks if the command succeeded
if !hasError || cmdsRanContains(cmd, cmdsRan) {
cmdToRun.ExecuteHooks("success", opts)
}
// Execute final hooks for every command
cmdToRun.ExecuteHooks("final", opts)
}
// Send the final result for the list
if hasError {
results <- CmdResult{CmdName: cmdsRan[len(cmdsRan)-1], ListName: list.Name, Error: fmt.Errorf("list execution failed")}
} else {
results <- CmdResult{CmdName: cmdsRan[len(cmdsRan)-1], ListName: list.Name, Error: nil}
}
}
}
// Helper to check if a command is in the list of executed commands
func cmdsRanContains(cmd string, cmdsRan []string) bool {
for _, c := range cmdsRan {
if c == cmd {
return true
}
}
return false
results <- res.ErrCmd
}
// Helper to notify errors
func notifyError(logger zerolog.Logger, templates *msgTemplates, list *CmdList, cmdsRan []string, outStructArr []outStruct, err error, cmd *Command) {
errStruct := map[string]interface{}{
"listName": list.Name,
"CmdsRan": cmdsRan,
"CmdOutput": outStructArr,
"Err": err,
"Command": cmd.Name,
"Args": cmd.Args,
}
var errMsg bytes.Buffer
if e := templates.err.Execute(&errMsg, errStruct); e != nil {
logger.Err(e).Send()
return
}
if e := list.NotifyConfig.Send(context.Background(), fmt.Sprintf("List %s failed", list.Name), errMsg.String()); e != nil {
logger.Err(e).Send()
}
}
// Helper to notify success
func notifySuccess(logger zerolog.Logger, templates *msgTemplates, list *CmdList, cmdsRan []string, outStructArr []outStruct) {
successStruct := map[string]interface{}{
"listName": list.Name,
"CmdsRan": cmdsRan,
"CmdOutput": outStructArr,
}
var successMsg bytes.Buffer
if e := templates.success.Execute(&successMsg, successStruct); e != nil {
logger.Err(e).Send()
return
}
if e := list.NotifyConfig.Send(context.Background(), fmt.Sprintf("List %s succeeded", list.Name), successMsg.String()); e != nil {
logger.Err(e).Send()
}
}
// RunListConfig runs a command list from the ConfigFile.
@ -268,35 +263,52 @@ func (opts *ConfigOpts) RunListConfig(cron string) {
}
configListsLen := len(opts.CmdConfigLists)
listChan := make(chan *CmdList, configListsLen)
results := make(chan CmdResult, configListsLen)
results := make(chan string)
// Start workers
// This starts up list workers, initially blocked
// because there are no jobs yet.
for w := 1; w <= configListsLen; w++ {
go cmdListWorker(mTemps, listChan, results, opts)
}
// Enqueue jobs
for listName, cmdConfig := range opts.CmdConfigLists {
if cmdConfig.Name == "" {
cmdConfig.Name = listName
}
if cron == "" || cron == cmdConfig.Cron {
if cron != "" {
if cron == cmdConfig.Cron {
listChan <- cmdConfig
}
} else {
listChan <- cmdConfig
}
}
close(listChan)
// Process results
for a := 1; a <= configListsLen; a++ {
result := <-results
opts.Logger.Debug().Msgf("Processing result for list %s, command %s", result.ListName, result.CmdName)
l := <-results
opts.Logger.Debug().Msg(l)
if l != "" {
// execute error hooks
opts.Logger.Debug().Msg("hooks are working")
opts.Cmds[l].ExecuteHooks("error", opts)
} else {
// execute success hooks
opts.Cmds[l].ExecuteHooks("success", opts)
// Process final hooks for the list (already handled in worker)
}
// execute final hooks
opts.Cmds[l].ExecuteHooks("final", opts)
}
opts.closeHostConnections()
}
func (opts *ConfigOpts) ExecuteCmds() {
func (config *ConfigOpts) ExecuteCmds(opts *ConfigOpts) {
for _, cmd := range opts.executeCmds {
cmdToRun := opts.Cmds[cmd]
cmdLogger := cmdToRun.GenerateLogger(opts)
@ -416,14 +428,3 @@ func (opts *ConfigOpts) ExecCmdsSSH(cmdList []string, hostsList []string) {
}
}
}
// func executeUserCommands() []string {
// }
// // parseRemoteSources parses source and validates fields using sourceType
// func (c *Command) parseRemoteSources(source, sourceType string) {
// switch sourceType {
// }
// }

View File

@ -2,19 +2,17 @@ package backy
import (
"context"
"errors"
"fmt"
"os"
"path"
"runtime"
"strings"
"git.andrewnw.xyz/CyberShell/backy/pkg/configfetcher"
"git.andrewnw.xyz/CyberShell/backy/pkg/logging"
"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman"
"git.andrewnw.xyz/CyberShell/backy/pkg/usermanager"
vault "github.com/hashicorp/vault/api"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/rawbytes"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/v2"
"github.com/mattn/go-isatty"
"github.com/rs/zerolog"
@ -28,75 +26,63 @@ var configFiles []string
const macroStart string = "%{"
const macroEnd string = "}%"
const envMacroStart string = "%{env:"
const vaultMacroStart string = "%{vault:"
const vaultMacroStart string = "%{env:"
func (opts *ConfigOpts) InitConfig() {
homeDir, err := os.UserHomeDir()
if err != nil {
logging.ExitWithMSG(err.Error(), 1, nil)
homeDir, homeDirErr = os.UserHomeDir()
if homeDirErr != nil {
fmt.Println(homeDirErr)
logging.ExitWithMSG(homeDirErr.Error(), 1, nil)
}
backyHomeConfDir := path.Join(homeDir, ".config/backy/")
configFiles := []string{
"./backy.yml", "./backy.yaml",
path.Join(backyHomeConfDir, "backy.yml"),
path.Join(backyHomeConfDir, "backy.yaml"),
}
backyHomeConfDir = homeDir + "/.config/backy/"
configFiles = []string{"./backy.yml", "./backy.yaml", backyHomeConfDir + "backy.yml", backyHomeConfDir + "backy.yaml"}
backyKoanf := koanf.New(".")
opts.ConfigFilePath = strings.TrimSpace(opts.ConfigFilePath)
// Initialize the fetcher
fetcher, err := configfetcher.NewConfigFetcher(opts.ConfigFilePath)
if opts.ConfigFilePath != "" {
err := testFile(opts.ConfigFilePath)
if err != nil {
logging.ExitWithMSG(fmt.Sprintf("error initializing config fetcher: %v", err), 1, nil)
logging.ExitWithMSG(fmt.Sprintf("Could not open config file %s: %v", opts.ConfigFilePath, err), 1, nil)
}
if opts.ConfigFilePath != "" {
loadConfigFile(fetcher, opts.ConfigFilePath, backyKoanf, opts)
if err := backyKoanf.Load(file.Provider(opts.ConfigFilePath), yaml.Parser()); err != nil {
logging.ExitWithMSG(fmt.Sprintf("error loading config: %v", err), 1, &opts.Logger)
}
} else {
loadDefaultConfigFiles(fetcher, configFiles, backyKoanf, opts)
cFileFailures := 0
for _, c := range configFiles {
if err := backyKoanf.Load(file.Provider(c), yaml.Parser()); err != nil {
cFileFailures++
} else {
opts.ConfigFilePath = c
break
}
}
if cFileFailures == len(configFiles) {
logging.ExitWithMSG(fmt.Sprintf("could not find a config file. Put one in the following paths: %v", configFiles), 1, &opts.Logger)
}
}
opts.koanf = backyKoanf
}
func loadConfigFile(fetcher configfetcher.ConfigFetcher, filePath string, k *koanf.Koanf, opts *ConfigOpts) {
data, err := fetcher.Fetch(filePath)
if err != nil {
logging.ExitWithMSG(fmt.Sprintf("Could not fetch config file %s: %v", filePath, err), 1, nil)
}
// ReadConfig validates and reads the config file.
func ReadConfig(opts *ConfigOpts) *ConfigOpts {
if err := k.Load(rawbytes.Provider(data), yaml.Parser()); err != nil {
logging.ExitWithMSG(fmt.Sprintf("error loading config: %v", err), 1, &opts.Logger)
if isatty.IsTerminal(os.Stdout.Fd()) {
os.Setenv("BACKY_TERM", "enabled")
} else if isatty.IsCygwinTerminal(os.Stdout.Fd()) {
os.Setenv("BACKY_TERM", "enabled")
} else {
os.Setenv("BACKY_TERM", "disabled")
}
}
func loadDefaultConfigFiles(fetcher configfetcher.ConfigFetcher, configFiles []string, k *koanf.Koanf, opts *ConfigOpts) {
cFileFailures := 0
for _, c := range configFiles {
data, err := fetcher.Fetch(c)
if err != nil {
cFileFailures++
continue
}
if err := k.Load(rawbytes.Provider(data), yaml.Parser()); err != nil {
cFileFailures++
continue
}
break
}
if cFileFailures == len(configFiles) {
logging.ExitWithMSG("Could not find any valid config file", 1, nil)
}
}
func (opts *ConfigOpts) ReadConfig() *ConfigOpts {
setTerminalEnv()
backyKoanf := opts.koanf
@ -108,39 +94,228 @@ func (opts *ConfigOpts) ReadConfig() *ConfigOpts {
CheckConfigValues(backyKoanf, opts.ConfigFilePath)
validateCommands(backyKoanf, opts)
// check for commands in file
for _, c := range opts.executeCmds {
if !backyKoanf.Exists(getCmdFromConfig(c)) {
logging.ExitWithMSG(Sprintf("command %s is not in config file %s", c, opts.ConfigFilePath), 1, nil)
}
}
setLoggingOptions(backyKoanf, opts)
// TODO: refactor this further down the line
// for _, l := range opts.executeLists {
// if !backyKoanf.Exists(getCmdListFromConfig(l)) {
// logging.ExitWithMSG(Sprintf("list %s not found", l), 1, nil)
// }
// }
// check for verbosity, via
// 1. config file
// 2. TODO: CLI flag
// 3. TODO: ENV var
var (
isLoggingVerbose bool
logFile string
)
isLoggingVerbose = backyKoanf.Bool(getLoggingKeyFromConfig("verbose"))
logFile = fmt.Sprintf("%s/backy.log", path.Dir(opts.ConfigFilePath)) // get full path to logfile
if backyKoanf.Exists(getLoggingKeyFromConfig("file")) {
logFile = backyKoanf.String(getLoggingKeyFromConfig("file"))
}
zerolog.SetGlobalLevel(zerolog.InfoLevel)
if isLoggingVerbose {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
globalLvl := zerolog.GlobalLevel()
os.Setenv("BACKY_LOGLEVEL", Sprintf("%v", globalLvl))
}
consoleLoggingDisabled := backyKoanf.Bool(getLoggingKeyFromConfig("console-disabled"))
os.Setenv("BACKY_CONSOLE_LOGGING", "enabled")
// Other qualifiers can go here as well
if consoleLoggingDisabled {
os.Setenv("BACKY_CONSOLE_LOGGING", "")
}
writers := logging.SetLoggingWriters(logFile)
log := zerolog.New(writers).With().Timestamp().Logger()
log := setupLogger(opts)
opts.Logger = log
log.Info().Str("config file", opts.ConfigFilePath).Send()
unmarshalConfig(backyKoanf, "commands", &opts.Cmds, opts.Logger)
unmarshalErr := backyKoanf.UnmarshalWithConf("commands", &opts.Cmds, koanf.UnmarshalConf{Tag: "yaml"})
validateCommandEnvironments(opts)
if unmarshalErr != nil {
unmarshalConfig(backyKoanf, "hosts", &opts.Hosts, opts.Logger)
panic(fmt.Errorf("error unmarshaling cmds struct: %w", unmarshalErr))
resolveHostConfigs(opts)
}
loadCommandLists(opts, backyKoanf)
for cmdName, cmdConf := range opts.Cmds {
envFileErr := testFile(cmdConf.Env)
if envFileErr != nil {
opts.Logger.Info().Str("cmd", cmdName).Err(envFileErr).Send()
os.Exit(1)
}
validateCommandLists(opts)
expandEnvVars(opts.backyEnv, cmdConf.Environment)
}
if opts.cronEnabled && len(opts.CmdConfigLists) == 0 {
// Get host configurations from config file
unmarshalErr = backyKoanf.UnmarshalWithConf("hosts", &opts.Hosts, koanf.UnmarshalConf{Tag: "yaml"})
if unmarshalErr != nil {
panic(fmt.Errorf("error unmarshalling hosts struct: %w", unmarshalErr))
}
for hostConfigName, host := range opts.Hosts {
if host.Host == "" {
host.Host = hostConfigName
}
if host.ProxyJump != "" {
proxyHosts := strings.Split(host.ProxyJump, ",")
for hostNum, h := range proxyHosts {
if hostNum > 1 {
proxyHost, defined := opts.Hosts[h]
if defined {
host.ProxyHost = append(host.ProxyHost, proxyHost)
} else {
newProxy := &Host{Host: h}
host.ProxyHost = append(host.ProxyHost, newProxy)
}
} else {
proxyHost, defined := opts.Hosts[h]
if defined {
host.ProxyHost = append(host.ProxyHost, proxyHost)
} else {
newHost := &Host{Host: h}
host.ProxyHost = append(host.ProxyHost, newHost)
}
}
}
}
}
// get command lists
// command lists should still be in the same file if no:
// 1. key 'cmd-lists.file' is found
// 2. hosts.yml or hosts.yaml is found in the same directory as the backy config file
backyConfigFileDir := path.Dir(opts.ConfigFilePath)
listsConfig := koanf.New(".")
listConfigFiles := []string{path.Join(backyConfigFileDir, "lists.yml"), path.Join(backyConfigFileDir, "lists.yaml")}
log.Info().Strs("list config files", listConfigFiles).Send()
for _, l := range listConfigFiles {
cFileFailures := 0
if err := listsConfig.Load(file.Provider(l), yaml.Parser()); err != nil {
cFileFailures++
} else {
opts.ConfigFilePath = l
break
}
if cFileFailures == len(configFiles) {
logging.ExitWithMSG(fmt.Sprintf("could not find a config file. Put one in the following paths: %v", listConfigFiles), 1, &opts.Logger)
// logging.ExitWithMSG((fmt.Sprintf("error unmarshalling cmd list struct: %v", unmarshalErr)), 1, &opts.Logger)
}
}
_ = listsConfig.UnmarshalWithConf("cmd-lists", &opts.CmdConfigLists, koanf.UnmarshalConf{Tag: "yaml"})
if backyKoanf.Exists("cmd-lists") {
unmarshalErr = backyKoanf.UnmarshalWithConf("cmd-lists", &opts.CmdConfigLists, koanf.UnmarshalConf{Tag: "yaml"})
// if unmarshalErr is not nil, look for a cmd-lists.file key
if unmarshalErr != nil {
// if file key exists, resolve file path and try to read and unmarshal file into command lists config
if backyKoanf.Exists("cmd-lists.file") {
opts.CmdListFile = strings.TrimSpace(backyKoanf.String("cmd-lists.file"))
cmdListFilePath := path.Clean(opts.CmdListFile)
// if path is not absolute, check config directory
if !strings.HasPrefix(cmdListFilePath, "/") {
opts.CmdListFile = path.Join(backyConfigFileDir, cmdListFilePath)
}
err := testFile(opts.CmdListFile)
if err != nil {
logging.ExitWithMSG(fmt.Sprintf("Could not open config file %s: %v. \n\nThe cmd-lists config should be in the main config file or should be in a lists.yml or lists.yaml file.", opts.CmdListFile, err), 1, nil)
}
if err := listsConfig.Load(file.Provider(opts.CmdListFile), yaml.Parser()); err != nil {
logging.ExitWithMSG(fmt.Sprintf("error loading config: %v", err), 1, &opts.Logger)
}
log.Info().Str("using lists config file", opts.CmdListFile).Send()
}
}
}
var cmdNotFoundSliceErr []error
for cmdListName, cmdList := range opts.CmdConfigLists {
if opts.cronEnabled {
cron := strings.TrimSpace(cmdList.Cron)
if cron == "" {
delete(opts.CmdConfigLists, cmdListName)
}
}
for _, cmdInList := range cmdList.Order {
_, cmdNameFound := opts.Cmds[cmdInList]
if !cmdNameFound {
cmdNotFoundStr := fmt.Sprintf("command %s in list %s is not defined in commands section in config file", cmdInList, cmdListName)
cmdNotFoundErr := errors.New(cmdNotFoundStr)
cmdNotFoundSliceErr = append(cmdNotFoundSliceErr, cmdNotFoundErr)
}
}
}
// Exit program if command is not found from list
if len(cmdNotFoundSliceErr) > 0 {
var cmdNotFoundErrorLog = log.Fatal()
cmdNotFoundErrorLog.Errs("commands not found", cmdNotFoundSliceErr).Send()
}
if opts.cronEnabled && (len(opts.CmdConfigLists) == 0) {
logging.ExitWithMSG("No cron fields detected in any command lists", 1, nil)
}
// process commands
if err := processCmds(opts); err != nil {
logging.ExitWithMSG(err.Error(), 1, &opts.Logger)
}
filterExecuteLists(opts)
if len(opts.executeLists) > 0 {
for l := range opts.CmdConfigLists {
if !contains(opts.executeLists, l) {
delete(opts.CmdConfigLists, l)
}
}
}
if backyKoanf.Exists("notifications") {
unmarshalConfig(backyKoanf, "notifications", &opts.NotificationConf, opts.Logger)
unmarshalErr = backyKoanf.UnmarshalWithConf("notifications", &opts.NotificationConf, koanf.UnmarshalConf{Tag: "yaml"})
if unmarshalErr != nil {
fmt.Printf("error unmarshalling notifications object: %v", unmarshalErr)
}
}
opts.SetupNotify()
@ -152,182 +327,6 @@ func (opts *ConfigOpts) ReadConfig() *ConfigOpts {
return opts
}
func setTerminalEnv() {
if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) {
os.Setenv("BACKY_TERM", "enabled")
} else {
os.Setenv("BACKY_TERM", "disabled")
}
}
func validateCommands(k *koanf.Koanf, opts *ConfigOpts) {
for _, c := range opts.executeCmds {
if !k.Exists(getCmdFromConfig(c)) {
logging.ExitWithMSG(fmt.Sprintf("command %s is not in config file %s", c, opts.ConfigFilePath), 1, nil)
}
}
}
func setLoggingOptions(k *koanf.Koanf, opts *ConfigOpts) {
isLoggingVerbose := k.Bool(getLoggingKeyFromConfig("verbose"))
// if log file is set in config file and not set on command line, use "./backy.log"
logFile := "./backy.log"
if opts.LogFilePath == "" && k.Exists(getLoggingKeyFromConfig("file")) {
logFile = k.String(getLoggingKeyFromConfig("file"))
opts.LogFilePath = logFile
}
zerolog.SetGlobalLevel(zerolog.InfoLevel)
if isLoggingVerbose {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
os.Setenv("BACKY_LOGLEVEL", fmt.Sprintf("%v", zerolog.GlobalLevel()))
}
if k.Bool(getLoggingKeyFromConfig("console-disabled")) {
os.Setenv("BACKY_CONSOLE_LOGGING", "")
} else {
os.Setenv("BACKY_CONSOLE_LOGGING", "enabled")
}
}
func setupLogger(opts *ConfigOpts) zerolog.Logger {
writers := logging.SetLoggingWriters(opts.LogFilePath)
return zerolog.New(writers).With().Timestamp().Logger()
}
func unmarshalConfig(k *koanf.Koanf, key string, target interface{}, log zerolog.Logger) {
if err := k.UnmarshalWithConf(key, target, koanf.UnmarshalConf{Tag: "yaml"}); err != nil {
logging.ExitWithMSG(fmt.Sprintf("error unmarshalling %s struct: %v", key, err), 1, &log)
}
}
func validateCommandEnvironments(opts *ConfigOpts) {
for cmdName, cmdConf := range opts.Cmds {
if err := testFile(cmdConf.Env); err != nil {
opts.Logger.Info().Str("cmd", cmdName).Err(err).Send()
os.Exit(1)
}
expandEnvVars(opts.backyEnv, cmdConf.Environment)
}
}
func resolveHostConfigs(opts *ConfigOpts) {
for hostConfigName, host := range opts.Hosts {
if host.Host == "" {
host.Host = hostConfigName
}
if host.ProxyJump != "" {
resolveProxyHosts(host, opts)
}
}
}
func resolveProxyHosts(host *Host, opts *ConfigOpts) {
proxyHosts := strings.Split(host.ProxyJump, ",")
for _, h := range proxyHosts {
proxyHost, defined := opts.Hosts[h]
if !defined {
proxyHost = &Host{Host: h}
opts.Hosts[h] = proxyHost
}
host.ProxyHost = append(host.ProxyHost, proxyHost)
}
}
func loadCommandLists(opts *ConfigOpts, backyKoanf *koanf.Koanf) {
backyConfigFileDir := path.Dir(opts.ConfigFilePath)
listsConfig := koanf.New(".")
listConfigFiles := []string{
path.Join(backyConfigFileDir, "lists.yml"),
path.Join(backyConfigFileDir, "lists.yaml"),
}
for _, l := range listConfigFiles {
if loadListConfigFile(l, listsConfig, opts) {
break
}
}
if backyKoanf.Exists("cmd-lists") {
unmarshalConfig(backyKoanf, "cmd-lists", &opts.CmdConfigLists, opts.Logger)
if backyKoanf.Exists("cmd-lists.file") {
loadCmdListsFile(backyKoanf, listsConfig, opts)
}
}
}
func loadListConfigFile(filePath string, k *koanf.Koanf, opts *ConfigOpts) bool {
fetcher, err := configfetcher.NewConfigFetcher(filePath)
if err != nil {
logging.ExitWithMSG(fmt.Sprintf("error initializing config fetcher: %v", err), 1, nil)
}
data, err := fetcher.Fetch(filePath)
if err != nil {
return false
}
if err := k.Load(rawbytes.Provider(data), yaml.Parser()); err != nil {
return false
}
opts.CmdListFile = filePath
return true
}
func loadCmdListsFile(backyKoanf *koanf.Koanf, listsConfig *koanf.Koanf, opts *ConfigOpts) {
opts.CmdListFile = strings.TrimSpace(backyKoanf.String("cmd-lists.file"))
if !path.IsAbs(opts.CmdListFile) {
opts.CmdListFile = path.Join(path.Dir(opts.ConfigFilePath), opts.CmdListFile)
}
fetcher, err := configfetcher.NewConfigFetcher(opts.CmdListFile)
if err != nil {
logging.ExitWithMSG(fmt.Sprintf("error initializing config fetcher: %v", err), 1, nil)
}
data, err := fetcher.Fetch(opts.CmdListFile)
if err != nil {
logging.ExitWithMSG(fmt.Sprintf("Could not fetch config file %s: %v", opts.CmdListFile, err), 1, nil)
}
if err := listsConfig.Load(rawbytes.Provider(data), yaml.Parser()); err != nil {
logging.ExitWithMSG(fmt.Sprintf("error loading config: %v", err), 1, &opts.Logger)
}
unmarshalConfig(listsConfig, "cmd-lists", &opts.CmdConfigLists, opts.Logger)
opts.Logger.Info().Str("using lists config file", opts.CmdListFile).Send()
}
func validateCommandLists(opts *ConfigOpts) {
var cmdNotFoundSliceErr []error
for cmdListName, cmdList := range opts.CmdConfigLists {
if opts.cronEnabled && strings.TrimSpace(cmdList.Cron) == "" {
delete(opts.CmdConfigLists, cmdListName)
continue
}
for _, cmdInList := range cmdList.Order {
if _, cmdNameFound := opts.Cmds[cmdInList]; !cmdNameFound {
cmdNotFoundSliceErr = append(cmdNotFoundSliceErr, fmt.Errorf("command %s in list %s is not defined in commands section in config file", cmdInList, cmdListName))
}
}
}
if len(cmdNotFoundSliceErr) > 0 {
opts.Logger.Fatal().Errs("commands not found", cmdNotFoundSliceErr).Send()
}
}
func filterExecuteLists(opts *ConfigOpts) {
if len(opts.executeLists) > 0 {
for l := range opts.CmdConfigLists {
if !contains(opts.executeLists, l) {
delete(opts.CmdConfigLists, l)
}
}
}
}
func getNestedConfig(nestedConfig, key string) string {
return fmt.Sprintf("%s.%s", nestedConfig, key)
}
@ -449,7 +448,6 @@ func GetVaultKey(str string, opts *ConfigOpts, log zerolog.Logger) string {
}
func processCmds(opts *ConfigOpts) error {
// process commands
for cmdName, cmd := range opts.Cmds {
@ -505,7 +503,7 @@ func processCmds(opts *ConfigOpts) error {
// Validate the operation
switch cmd.PackageOperation {
case "install", "remove", "upgrade", "checkVersion":
case "install", "remove", "upgrade":
cmd.pkgMan, err = pkgman.PackageManagerFactory(cmd.PackageManager, pkgman.WithoutAuth())
if err != nil {
return err
@ -513,33 +511,6 @@ func processCmds(opts *ConfigOpts) error {
default:
return fmt.Errorf("unsupported package operation %s for command %s", cmd.PackageOperation, cmd.Name)
}
}
// Parse user commands
if cmd.Type == "user" {
if cmd.Username == "" {
return fmt.Errorf("username is required for user command %s", cmd.Name)
}
detectOSType(cmd, opts)
var err error
// Validate the operation
switch cmd.UserOperation {
case "add", "remove", "modify", "checkIfExists", "delete", "password":
cmd.userMan, err = usermanager.NewUserManager(cmd.OS)
if cmd.Host != nil {
host, ok := opts.Hosts[*cmd.Host]
if ok {
cmd.userMan, err = usermanager.NewUserManager(host.OS)
}
}
if err != nil {
return err
}
default:
return fmt.Errorf("unsupported user operation %s for command %s", cmd.UserOperation, cmd.Name)
}
}
}
@ -586,32 +557,3 @@ func processHooks(cmd *Command, hooks []string, opts *ConfigOpts, hookType strin
}
return nil
}
func detectOSType(cmd *Command, opts *ConfigOpts) error {
if cmd.Host == nil {
if runtime.GOOS == "linux" { // also can be specified to FreeBSD
cmd.OS = "linux"
opts.Logger.Info().Msg("Unix/Linux type OS detected")
}
}
host, ok := opts.Hosts[*cmd.Host]
if ok {
if host.OS != "" {
return nil
}
os, err := host.DetectOS(opts)
os = strings.TrimSpace(os)
if err != nil {
return err
}
if os == "" {
return fmt.Errorf("error detecting os for command %s: empty string", cmd.Name)
}
if strings.Contains(os, "linux") {
os = "linux"
}
host.OS = os
}
return nil
}

View File

@ -119,7 +119,7 @@ func (remoteConfig *Host) ConnectToHost(opts *ConfigOpts) error {
return errors.Wrap(err, "could not create hostkeycallback function")
}
remoteConfig.ClientConfig.HostKeyCallback = hostKeyCallback
// opts.Logger.Info().Str("user", remoteConfig.ClientConfig.User).Send()
opts.Logger.Info().Str("user", remoteConfig.ClientConfig.User).Send()
remoteConfig.SshClient, connectErr = remoteConfig.ConnectThroughBastion(opts.Logger)
if connectErr != nil {
@ -492,10 +492,9 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts)
env: command.Environment,
}
)
// Get the command type
// This must be done before concatenating the arguments
command.Type = strings.TrimSpace(command.Type)
command = getCommandType(command)
command = getPackageCommand(command)
// Prepare command arguments
for _, v := range command.Args {
@ -507,7 +506,7 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts)
Str("Host", *command.Host).
Msgf("Running %s on host %s", getCommandTypeLabel(command.Type), *command.Host)
// cmdCtxLogger.Debug().Str("cmd", command.Cmd).Strs("args", command.Args).Send()
cmdCtxLogger.Debug().Str("cmd", command.Cmd).Strs("args", command.Args).Send()
// Ensure SSH client is connected
if command.RemoteHost.SshClient == nil {
@ -517,7 +516,7 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts)
}
// Create new SSH session
commandSession, err := command.RemoteHost.createSSHSession(opts)
commandSession, err := command.createSSHSession(opts)
if err != nil {
return nil, fmt.Errorf("failed to create SSH session: %w", err)
}
@ -540,27 +539,6 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts)
return command.runScript(commandSession, cmdCtxLogger, &cmdOutBuf)
case "scriptFile":
return command.runScriptFile(commandSession, cmdCtxLogger, &cmdOutBuf)
case "package":
if command.PackageOperation == "checkVersion" {
commandSession.Stderr = nil
// Execute the package version command remotely
// Parse the output of package version command
// Compare versions
// Check if a specific version is specified
commandSession.Stdout = nil
return checkPackageVersion(cmdCtxLogger, command, commandSession, cmdOutBuf)
} else {
if command.Shell != "" {
ArgsStr = fmt.Sprintf("%s -c '%s %s'", command.Shell, command.Cmd, ArgsStr)
} else {
ArgsStr = fmt.Sprintf("%s %s", command.Cmd, ArgsStr)
}
cmdCtxLogger.Debug().Str("cmd + args", ArgsStr).Send()
// Run simple command
if err := commandSession.Run(ArgsStr); err != nil {
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, true), fmt.Errorf("error running command: %w", err)
}
}
default:
if command.Shell != "" {
ArgsStr = fmt.Sprintf("%s -c '%s %s'", command.Shell, command.Cmd, ArgsStr)
@ -570,35 +548,11 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts)
cmdCtxLogger.Debug().Str("cmd + args", ArgsStr).Send()
// Run simple command
if err := commandSession.Run(ArgsStr); err != nil {
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.GetOutput), fmt.Errorf("error running command: %w", err)
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger), fmt.Errorf("error running command: %w", err)
}
}
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, true), nil
}
func checkPackageVersion(cmdCtxLogger zerolog.Logger, command *Command, commandSession *ssh.Session, cmdOutBuf bytes.Buffer) ([]string, error) {
cmdCtxLogger.Info().Str("package", command.PackageName).Msg("Checking package versions")
// Prepare command arguments
ArgsStr := command.Cmd
for _, v := range command.Args {
ArgsStr += fmt.Sprintf(" %s", v)
}
var err error
var cmdOut []byte
if cmdOut, err = commandSession.CombinedOutput(ArgsStr); err != nil {
cmdOutBuf.Write(cmdOut)
_, parseErr := parsePackageVersion(string(cmdOut), cmdCtxLogger, command, cmdOutBuf)
if parseErr != nil {
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.GetOutput), fmt.Errorf("error: package %s not listed: %w", command.PackageName, err)
}
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.GetOutput), fmt.Errorf("error running %s: %w", ArgsStr, err)
}
return parsePackageVersion(string(cmdOut), cmdCtxLogger, command, cmdOutBuf)
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger), nil
}
// getCommandTypeLabel returns a human-readable label for the command type.
@ -609,6 +563,20 @@ func getCommandTypeLabel(commandType string) string {
return fmt.Sprintf("%s command", commandType)
}
// createSSHSession attempts to create a new SSH session and retries on failure.
func (command *Command) createSSHSession(opts *ConfigOpts) (*ssh.Session, error) {
session, err := command.RemoteHost.SshClient.NewSession()
if err == nil {
return session, nil
}
// Retry connection and session creation
if connErr := command.RemoteHost.ConnectToHost(opts); connErr != nil {
return nil, fmt.Errorf("session creation failed: %v, connection retry failed: %v", err, connErr)
}
return command.RemoteHost.SshClient.NewSession()
}
// runScript handles the execution of inline scripts.
func (command *Command) runScript(session *ssh.Session, cmdCtxLogger zerolog.Logger, outputBuf *bytes.Buffer) ([]string, error) {
script, err := command.prepareScriptBuffer()
@ -622,10 +590,10 @@ func (command *Command) runScript(session *ssh.Session, cmdCtxLogger zerolog.Log
}
if err := session.Wait(); err != nil {
return collectOutput(outputBuf, command.Name, cmdCtxLogger, true), fmt.Errorf("error waiting for shell: %w", err)
return collectOutput(outputBuf, command.Name, cmdCtxLogger), fmt.Errorf("error waiting for shell: %w", err)
}
return collectOutput(outputBuf, command.Name, cmdCtxLogger, command.GetOutput), nil
return collectOutput(outputBuf, command.Name, cmdCtxLogger), nil
}
// runScriptFile handles the execution of script files.
@ -641,10 +609,10 @@ func (command *Command) runScriptFile(session *ssh.Session, cmdCtxLogger zerolog
}
if err := session.Wait(); err != nil {
return collectOutput(outputBuf, command.Name, cmdCtxLogger, true), fmt.Errorf("error waiting for shell: %w", err)
return collectOutput(outputBuf, command.Name, cmdCtxLogger), fmt.Errorf("error waiting for shell: %w", err)
}
return collectOutput(outputBuf, command.Name, cmdCtxLogger, true), nil
return collectOutput(outputBuf, command.Name, cmdCtxLogger), nil
}
// prepareScriptBuffer prepares a buffer for inline scripts.
@ -709,51 +677,13 @@ func readFileToBuffer(filePath string) (*bytes.Buffer, error) {
}
// collectOutput collects output from a buffer and logs it.
func collectOutput(buf *bytes.Buffer, commandName string, logger zerolog.Logger, wantOutput bool) []string {
func collectOutput(buf *bytes.Buffer, commandName string, logger zerolog.Logger) []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.
func (h *Host) createSSHSession(opts *ConfigOpts) (*ssh.Session, error) {
session, err := h.SshClient.NewSession()
if err == nil {
return session, nil
}
// Retry connection and session creation
if connErr := h.ConnectToHost(opts); connErr != nil {
return nil, fmt.Errorf("session creation failed: %v, connection retry failed: %v", err, connErr)
}
return h.SshClient.NewSession()
}
func (h *Host) DetectOS(opts *ConfigOpts) (string, error) {
err := h.ConnectToHost(opts)
if err != nil {
return "", err
}
var session *ssh.Session
session, err = h.createSSHSession(opts)
if err != nil {
return "", err
}
// Execute the "uname -a" command on the remote machine
output, err := session.CombinedOutput("uname")
if err != nil {
return "", fmt.Errorf("failed to execute OS detection command: %v", err)
}
// Parse the output to determine the OS
osName := string(output)
return osName, nil
}

View File

@ -15,7 +15,7 @@ The following commands ran:
{{end}}
{{ end }}
{{ if .CmdOutput }}{{- range .CmdOutput }}{{ printf "\n"}}Command output for {{ .CmdName }}:
{{ if .CmdOutput }}{{- range .CmdOutput }}Command output for {{ .CmdName }}:
{{- range .Output}}
{{ . }}
{{ end }}{{ end }}

View File

@ -5,7 +5,7 @@ The following commands ran:
- {{. -}}
{{end}}
{{ if .CmdOutput }}{{- range .CmdOutput }}{{ printf "\n"}}Command output for {{ .CmdName }}:
{{ if .CmdOutput }}{{- range .CmdOutput }}Command output for {{ .CmdName }}:
{{- range .Output}}
{{ . }}
{{ end }}{{ end }}

View File

@ -4,10 +4,7 @@ import (
"bytes"
"text/template"
"strings"
"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman"
"git.andrewnw.xyz/CyberShell/backy/pkg/usermanager"
vaultapi "github.com/hashicorp/vault/api"
"github.com/kevinburke/ssh_config"
"github.com/knadh/koanf/v2"
@ -21,7 +18,6 @@ type (
// Host defines a host to which to connect.
// If not provided, the values will be looked up in the default ssh config files
Host struct {
OS string `yaml:"OS,omitempty"`
ConfigFilePath string `yaml:"config,omitempty"`
Host string `yaml:"host,omitempty"`
HostName string `yaml:"hostname,omitempty"`
@ -118,32 +114,20 @@ type (
// Username specifies the username for user creation or related operations
Username string `yaml:"username,omitempty"`
// UserGroups specifies the groups to add the user to
UserGroups []string `yaml:"userGroups,omitempty"`
// Groups specifies the groups to add the user to
Groups []string `yaml:"groups,omitempty"`
// UserHome specifies the home directory for the user
UserHome string `yaml:"userHome,omitempty"`
// Home specifies the home directory for the user
Home string `yaml:"home,omitempty"`
// UserShell specifies the shell for the user
UserShell string `yaml:"userShell,omitempty"`
// System specifies whether the user is a system account
System bool `yaml:"system,omitempty"`
// SystemUser specifies whether the user is a system account
SystemUser bool `yaml:"systemUser,omitempty"`
// Password specifies the password for the user (can be file: or plain text)
Password string `yaml:"password,omitempty"`
// UserPassword specifies the password for the user (can be file: or plain text)
UserPassword string `yaml:"userPassword,omitempty"`
userMan usermanager.UserManager
// OS for the command, only used when type is user
OS string `yaml:"OS,omitempty"`
// UserOperation specifies the action for user-related commands (e.g., "create" or "remove")
UserOperation string `yaml:"userOperation,omitempty"`
userCmdSet bool
stdin *strings.Reader
// Operation specifies the action for user-related commands (e.g., "create" or "remove")
Operation string `yaml:"operation,omitempty"`
}
RemoteSource struct {
@ -192,9 +176,6 @@ type (
// Holds config file
ConfigFilePath string
// Holds log file
LogFilePath string
// for command list file
CmdListFile string
@ -271,9 +252,10 @@ type (
Final []string `yaml:"final,omitempty"`
}
CmdResult struct {
CmdName string // Name of the command executed
ListName string // Name of the command list
Error error // Error encountered, if any
CmdListResults struct {
// name of the list
ListName string
// command that caused the list to fail
ErrCmd string
}
)

View File

@ -5,7 +5,6 @@
package backy
import (
"bytes"
"errors"
"fmt"
"os"
@ -57,13 +56,6 @@ func SetCmdsToSearch(cmds []string) BackyOptionFunc {
}
}
// SetLogFile sets the path to the log file
func SetLogFile(logFile string) BackyOptionFunc {
return func(bco *ConfigOpts) {
bco.LogFilePath = logFile
}
}
// cronEnabled enables the execution of command lists at specified times
func CronEnabled() BackyOptionFunc {
return func(bco *ConfigOpts) {
@ -242,10 +234,9 @@ func expandEnvVars(backyEnv map[string]string, envVars []string) {
}
}
// getCommandType checks for command type and if the command has already been set
// Checks for types package and user
// Returns the modified Command with the package- or userManager command as Cmd and the package- or userOperation as args, plus any additional Args
func getCommandType(command *Command) *Command {
// getPackageCommand checks for command type of package and if the command has already been set
// Returns the modified Command with the packageManager command as Cmd and the packageOperation as args, plus any additional Args
func getPackageCommand(command *Command) *Command {
if command.Type == "package" && !command.packageCmdSet {
command.packageCmdSet = true
@ -256,69 +247,9 @@ func getCommandType(command *Command) *Command {
command.Cmd, command.Args = command.pkgMan.Remove(command.PackageName, command.Args)
case "upgrade":
command.Cmd, command.Args = command.pkgMan.Upgrade(command.PackageName, command.PackageVersion)
case "checkVersion":
command.Cmd, command.Args = command.pkgMan.CheckVersion(command.PackageName, command.PackageVersion)
}
} else if command.Type != "package" {
command.packageCmdSet = false
}
if command.Type == "user" && !command.userCmdSet {
command.userCmdSet = true
switch command.UserOperation {
case "add":
command.Cmd, command.Args = command.userMan.AddUser(
command.Username,
command.UserHome,
command.UserShell,
command.SystemUser,
command.UserGroups,
command.Args)
case "modify":
command.Cmd, command.Args = command.userMan.ModifyUser(
command.Username,
homeDir,
command.UserShell,
command.UserGroups)
case "checkIfExists":
command.Cmd, command.Args = command.userMan.UserExists(command.Username)
case "delete":
command.Cmd, command.Args = command.userMan.RemoveUser(command.Username)
case "password":
command.Cmd, command.stdin, command.UserPassword = command.userMan.ModifyPassword(command.Username, command.UserPassword)
}
}
return command
}
func parsePackageVersion(output string, cmdCtxLogger zerolog.Logger, command *Command, cmdOutBuf bytes.Buffer) ([]string, error) {
var err error
pkgVersion, err := command.pkgMan.Parse(output)
// println(output)
if err != nil {
cmdCtxLogger.Error().Err(err).Str("package", command.PackageName).Msg("Error parsing package version output")
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.GetOutput), err
}
cmdCtxLogger.Info().
Str("Installed", pkgVersion.Installed).
Str("Candidate", pkgVersion.Candidate).
Msg("Package version comparison")
if command.PackageVersion != "" {
if pkgVersion.Installed == command.PackageVersion {
cmdCtxLogger.Info().Msgf("Installed version matches specified version: %s", command.PackageVersion)
} else {
cmdCtxLogger.Info().Msgf("Installed version does not match specified version: %s", command.PackageVersion)
err = fmt.Errorf("Installed version does not match specified version: %s", command.PackageVersion)
}
} else {
if pkgVersion.Installed == pkgVersion.Candidate {
cmdCtxLogger.Info().Msg("Installed and Candidate versions match")
} else {
cmdCtxLogger.Info().Msg("Installed and Candidate versions differ")
err = errors.New("Installed and Candidate versions differ")
}
}
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, false), err
}

View File

@ -1,27 +0,0 @@
package configfetcher
import "strings"
type ConfigFetcher interface {
// Fetch retrieves the configuration from the specified URL or source
// Returns the raw data as bytes or an error
Fetch(source string) ([]byte, error)
// Parse decodes the raw data into a Go structure (e.g., Commands, CommandLists)
// Takes the raw data as input and populates the target interface
Parse(data []byte, target interface{}) error
}
func NewConfigFetcher(source string, options ...Option) (ConfigFetcher, error) {
if strings.HasPrefix(source, "http") || strings.HasPrefix(source, "https") {
return NewHTTPFetcher(options...), nil
} else if strings.HasPrefix(source, "s3") {
fetcher, err := NewS3Fetcher(options...)
if err != nil {
return nil, err
}
return fetcher, nil
} else {
return &LocalFetcher{}, nil
}
}

View File

@ -1,48 +0,0 @@
package configfetcher
import (
"errors"
"io"
"net/http"
"gopkg.in/yaml.v3"
)
type HTTPFetcher struct {
HTTPClient *http.Client
}
// NewHTTPFetcher creates a new instance of HTTPFetcher with the provided options.
func NewHTTPFetcher(options ...Option) *HTTPFetcher {
cfg := &FetcherConfig{}
for _, opt := range options {
opt(cfg)
}
// Initialize HTTP client if not provided
if cfg.HTTPClient == nil {
cfg.HTTPClient = http.DefaultClient
}
return &HTTPFetcher{HTTPClient: cfg.HTTPClient}
}
// Fetch retrieves the configuration from the specified URL
func (h *HTTPFetcher) Fetch(source string) ([]byte, error) {
resp, err := http.Get(source)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.New("failed to fetch remote config: " + resp.Status)
}
return io.ReadAll(resp.Body)
}
// Parse decodes the raw data into the provided target structure
func (h *HTTPFetcher) Parse(data []byte, target interface{}) error {
return yaml.Unmarshal(data, target)
}

View File

@ -1,26 +0,0 @@
package configfetcher
import (
"io"
"os"
"gopkg.in/yaml.v3"
)
type LocalFetcher struct{}
// Fetch retrieves the configuration from the specified local file path
func (l *LocalFetcher) Fetch(source string) ([]byte, error) {
file, err := os.Open(source)
if err != nil {
return nil, err
}
defer file.Close()
return io.ReadAll(file)
}
// Parse decodes the raw data into the provided target structure
func (l *LocalFetcher) Parse(data []byte, target interface{}) error {
return yaml.Unmarshal(data, target)
}

View File

@ -1,30 +0,0 @@
package configfetcher
import (
"net/http"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
// Option is a function that configures a fetcher.
type Option func(*FetcherConfig)
// FetcherConfig holds the configuration for a fetcher.
type FetcherConfig struct {
S3Client *s3.Client
HTTPClient *http.Client
}
// WithS3Client sets the S3 client for the fetcher.
func WithS3Client(client *s3.Client) Option {
return func(cfg *FetcherConfig) {
cfg.S3Client = client
}
}
// WithHTTPClient sets the HTTP client for the fetcher.
func WithHTTPClient(client *http.Client) Option {
return func(cfg *FetcherConfig) {
cfg.HTTPClient = client
}
}

View File

@ -1,75 +0,0 @@
package configfetcher
import (
"bytes"
"context"
"errors"
"strings"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
"gopkg.in/yaml.v3"
)
type S3Fetcher struct {
S3Client *s3.Client
}
// NewS3Fetcher creates a new instance of S3Fetcher with the provided options.
func NewS3Fetcher(options ...Option) (*S3Fetcher, error) {
cfg := &FetcherConfig{}
for _, opt := range options {
opt(cfg)
}
// Initialize S3 client if not provided
if cfg.S3Client == nil {
awsCfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
return nil, err
}
cfg.S3Client = s3.NewFromConfig(awsCfg)
}
return &S3Fetcher{S3Client: cfg.S3Client}, nil
}
// Fetch retrieves the configuration from an S3 bucket
// Source should be in the format "bucket-name/object-key"
func (s *S3Fetcher) Fetch(source string) ([]byte, error) {
bucket, key, err := parseS3Source(source)
if err != nil {
return nil, err
}
resp, err := s.S3Client.GetObject(context.TODO(), &s3.GetObjectInput{
Bucket: &bucket,
Key: &key,
})
if err != nil {
return nil, err
}
defer resp.Body.Close()
buf := new(bytes.Buffer)
_, err = buf.ReadFrom(resp.Body)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// Parse decodes the raw data into the provided target structure
func (s *S3Fetcher) Parse(data []byte, target interface{}) error {
return yaml.Unmarshal(data, target)
}
// Helper function to parse S3 source into bucket and key
func parseS3Source(source string) (bucket, key string, err error) {
parts := strings.SplitN(source, "/", 2)
if len(parts) != 2 {
return "", "", errors.New("invalid S3 source format, expected bucket-name/object-key")
}
return parts[0], parts[1], nil
}

View File

@ -2,8 +2,6 @@ package apt
import (
"fmt"
"regexp"
"strings"
"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/pkgcommon"
)
@ -12,7 +10,6 @@ import (
type AptManager struct {
useAuth bool // Whether to use an authentication command
authCommand string // The authentication command, e.g., "sudo"
Parser pkgcommon.PackageParser
}
// DefaultAuthCommand is the default command used for authentication.
@ -40,7 +37,6 @@ func (a *AptManager) Install(pkg, version string, args []string) (string, []stri
if args != nil {
baseArgs = append(baseArgs, args...)
}
fmt.Printf("baseArgs: %v\n", baseArgs)
return baseCmd, baseArgs
}
@ -66,14 +62,6 @@ func (a *AptManager) Upgrade(pkg, version string) (string, []string) {
return baseCmd, baseArgs
}
// CheckVersion returns the command and arguments for checking the info of a specific package.
func (a *AptManager) CheckVersion(pkg, version string) (string, []string) {
baseCmd := a.prependAuthCommand("apt-cache")
baseArgs := []string{"policy", pkg}
return baseCmd, baseArgs
}
// UpgradeAll returns the command and arguments for upgrading all packages.
func (a *AptManager) UpgradeAll() (string, []string) {
baseCmd := a.prependAuthCommand(DefaultPackageCommand)
@ -105,27 +93,3 @@ func (a *AptManager) SetUseAuth(useAuth bool) {
func (a *AptManager) SetAuthCommand(authCommand string) {
a.authCommand = authCommand
}
// Parse parses the apt-cache policy output to extract Installed and Candidate versions.
func (a *AptManager) Parse(output string) (*pkgcommon.PackageVersion, error) {
// Check for error message in the output
if strings.Contains(output, "Unable to locate package") {
return nil, fmt.Errorf("error: %s", strings.TrimSpace(output))
}
reInstalled := regexp.MustCompile(`Installed:\s*([^\s]+)`)
reCandidate := regexp.MustCompile(`Candidate:\s*([^\s]+)`)
installedMatch := reInstalled.FindStringSubmatch(output)
candidateMatch := reCandidate.FindStringSubmatch(output)
if len(installedMatch) < 2 || len(candidateMatch) < 2 {
return nil, fmt.Errorf("failed to parse Installed or Candidate versions from apt output. check package name")
}
return &pkgcommon.PackageVersion{
Installed: strings.TrimSpace(installedMatch[1]),
Candidate: strings.TrimSpace(candidateMatch[1]),
Match: installedMatch[1] == candidateMatch[1],
}, nil
}

View File

@ -2,8 +2,6 @@ package dnf
import (
"fmt"
"regexp"
"strings"
"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/pkgcommon"
)
@ -76,50 +74,6 @@ func (y *DnfManager) UpgradeAll() (string, []string) {
return baseCmd, baseArgs
}
// CheckVersion returns the command and arguments for checking the info of a specific package.
func (d *DnfManager) CheckVersion(pkg, version string) (string, []string) {
baseCmd := d.prependAuthCommand("dnf")
baseArgs := []string{"info", pkg}
return baseCmd, baseArgs
}
// Parse parses the dnf info output to extract Installed and Candidate versions.
func (d DnfManager) Parse(output string) (*pkgcommon.PackageVersion, error) {
// Check for error message in the output
if strings.Contains(output, "No matching packages to list") {
return nil, fmt.Errorf("error: package not listed")
}
// Define regular expressions to capture installed and available versions
reInstalled := regexp.MustCompile(`(?m)^Installed packages\s*Name\s*:\s*\S+\s*Epoch\s*:\s*\S+\s*Version\s*:\s*([^\s]+)\s*Release\s*:\s*([^\s]+)`)
reAvailable := regexp.MustCompile(`(?m)^Available packages\s*Name\s*:\s*\S+\s*Epoch\s*:\s*\S+\s*Version\s*:\s*([^\s]+)\s*Release\s*:\s*([^\s]+)`)
installedMatch := reInstalled.FindStringSubmatch(output)
candidateMatch := reAvailable.FindStringSubmatch(output)
installedVersion := ""
candidateVersion := ""
if len(installedMatch) >= 3 {
installedVersion = fmt.Sprintf("%s-%s", installedMatch[1], installedMatch[2])
}
if len(candidateMatch) >= 3 {
candidateVersion = fmt.Sprintf("%s-%s", candidateMatch[1], candidateMatch[2])
}
if installedVersion == "" && candidateVersion == "" {
return nil, fmt.Errorf("failed to parse versions from dnf output")
}
return &pkgcommon.PackageVersion{
Installed: installedVersion,
Candidate: candidateVersion,
}, nil
}
// prependAuthCommand prepends the authentication command if UseAuth is true.
func (y *DnfManager) prependAuthCommand(baseCmd string) string {
if y.useAuth {

View File

@ -2,16 +2,3 @@ package pkgcommon
// PackageManagerOption defines a functional option for configuring a PackageManager.
type PackageManagerOption func(interface{})
// PackageParser defines an interface for parsing package version information.
type PackageParser interface {
Parse(output string) (*PackageVersion, error)
}
// PackageVersion represents the installed and candidate versions of a package.
type PackageVersion struct {
Installed string
Candidate string
Match bool
Message string
}

View File

@ -15,8 +15,7 @@ type PackageManager interface {
Remove(pkg string, args []string) (string, []string)
Upgrade(pkg, version string) (string, []string) // Upgrade a specific package
UpgradeAll() (string, []string)
CheckVersion(pkg, version string) (string, []string)
Parse(output string) (*pkgcommon.PackageVersion, error)
// Configure applies functional options to customize the package manager.
Configure(options ...pkgcommon.PackageManagerOption)
}
@ -68,8 +67,6 @@ func WithoutAuth() pkgcommon.PackageManagerOption {
// ConfigurablePackageManager defines methods for setting configuration options.
type ConfigurablePackageManager interface {
pkgcommon.PackageParser
SetUseAuth(useAuth bool)
SetAuthCommand(authCommand string)
SetPackageParser(parser pkgcommon.PackageParser)
}

View File

@ -2,7 +2,6 @@ package yum
import (
"fmt"
"regexp"
"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/pkgcommon"
)
@ -75,43 +74,6 @@ func (y *YumManager) UpgradeAll() (string, []string) {
return baseCmd, baseArgs
}
// CheckVersion returns the command and arguments for checking the info of a specific package.
func (y *YumManager) CheckVersion(pkg, version string) (string, []string) {
baseCmd := y.prependAuthCommand("yum")
baseArgs := []string{"info", pkg}
return baseCmd, baseArgs
}
// Parse parses the dnf info output to extract Installed and Candidate versions.
func (y YumManager) Parse(output string) (*pkgcommon.PackageVersion, error) {
reInstalled := regexp.MustCompile(`(?m)^Installed Packages\s*Name\s*:\s*\S+\s*Version\s*:\s*([^\s]+)\s*Release\s*:\s*([^\s]+)`)
reAvailable := regexp.MustCompile(`(?m)^Available Packages\s*Name\s*:\s*\S+\s*Version\s*:\s*([^\s]+)\s*Release\s*:\s*([^\s]+)`)
installedMatch := reInstalled.FindStringSubmatch(output)
candidateMatch := reAvailable.FindStringSubmatch(output)
installedVersion := ""
candidateVersion := ""
if len(installedMatch) >= 3 {
installedVersion = fmt.Sprintf("%s-%s", installedMatch[1], installedMatch[2])
}
if len(candidateMatch) >= 3 {
candidateVersion = fmt.Sprintf("%s-%s", candidateMatch[1], candidateMatch[2])
}
if installedVersion == "" && candidateVersion == "" {
return nil, fmt.Errorf("failed to parse versions from dnf output")
}
return &pkgcommon.PackageVersion{
Installed: installedVersion,
Candidate: candidateVersion,
}, nil
}
// prependAuthCommand prepends the authentication command if UseAuth is true.
func (y *YumManager) prependAuthCommand(baseCmd string) string {
if y.useAuth {

View File

@ -1,90 +0,0 @@
package linux
import (
"fmt"
"strings"
passGen "github.com/sethvargo/go-password/password"
)
// LinuxUserManager implements UserManager for Linux systems.
type LinuxUserManager struct{}
func (l LinuxUserManager) NewLinuxManager() *LinuxUserManager {
return &LinuxUserManager{}
}
// AddUser adds a new user to the system.
func (l LinuxUserManager) AddUser(username, homeDir, shell string, isSystem bool, groups, args []string) (string, []string) {
baseArgs := []string{}
if isSystem {
baseArgs = append(baseArgs, "--system")
}
if homeDir != "" {
baseArgs = append(baseArgs, "--home", homeDir)
}
if shell != "" {
baseArgs = append(baseArgs, "--shell", shell)
}
if len(groups) > 0 {
baseArgs = append(baseArgs, "--groups", strings.Join(groups, ","))
}
if len(args) > 0 {
baseArgs = append(baseArgs, args...)
}
args = append(baseArgs, username)
cmd := "useradd"
return cmd, args
}
func (l LinuxUserManager) ModifyPassword(username, password string) (string, *strings.Reader, string) {
cmd := "chpasswd"
if password == "" {
password = passGen.MustGenerate(20, 5, 5, false, false)
}
stdin := strings.NewReader(fmt.Sprintf("%s:%s", username, password))
return cmd, stdin, password
}
// RemoveUser removes an existing user from the system.
func (l LinuxUserManager) RemoveUser(username string) (string, []string) {
cmd := "userdel"
return cmd, []string{username}
}
// ModifyUser modifies an existing user's details.
func (l LinuxUserManager) ModifyUser(username, homeDir, shell string, groups []string) (string, []string) {
args := []string{}
if homeDir != "" {
args = append(args, "--home", homeDir)
}
if shell != "" {
args = append(args, "--shell", shell)
}
if len(groups) > 0 {
args = append(args, "--groups", strings.Join(groups, ","))
}
args = append(args, username)
cmd := "usermod"
return cmd, args
}
// UserExists checks if a user exists on the system.
func (l LinuxUserManager) UserExists(username string) (string, []string) {
cmd := "id"
return cmd, []string{username}
}

View File

@ -1,35 +0,0 @@
package usermanager
import (
"fmt"
"strings"
"git.andrewnw.xyz/CyberShell/backy/pkg/usermanager/linux"
)
// UserManager defines the interface for user management operations.
// All functions but one return a string for the command and any args.
type UserManager interface {
AddUser(username, homeDir, shell string, isSystem bool, groups, args []string) (string, []string)
RemoveUser(username string) (string, []string)
ModifyUser(username, homeDir, shell string, groups []string) (string, []string)
// Modify password uses chpasswd for Linux systems to build the command to change the password
// Should return a password as the last argument
// TODO: refactor when adding more systems instead of Linux
ModifyPassword(username, password string) (string, *strings.Reader, string)
UserExists(username string) (string, []string)
}
func NewUserManager(system string) (UserManager, error) {
var manager UserManager
switch system {
case "linux", "Linux":
manager = linux.LinuxUserManager{}
default:
return nil, fmt.Errorf("usermanger system %s is not recognized", system)
}
return manager, nil
}