From 5c2bfcc9402a115b0e9cbdd19f4791b9f438dd92 Mon Sep 17 00:00:00 2001 From: Andrew Woodlee Date: Tue, 14 Jan 2025 09:42:43 -0600 Subject: [PATCH] [WIP] v0.7.0 --- .../unreleased/Added-20250111-211546.yaml | 3 + .../unreleased/Added-20250111-211813.yaml | 3 + .../unreleased/Added-20250113-231248.yaml | 3 + .../unreleased/Changed-20250113-231007.yaml | 3 + .../unreleased/Changed-20250113-231622.yaml | 3 + .changes/v0.6.1.md | 2 +- .vscode/settings.json | 1 + cmd/backup.go | 3 +- cmd/cron.go | 3 +- cmd/exec.go | 6 +- cmd/host.go | 4 +- cmd/list.go | 5 +- cmd/root.go | 3 + examples/backy.yaml | 5 +- go.mod | 27 +- go.sum | 59 ++ pkg/backy/backy.go | 92 +-- pkg/backy/config.go | 538 ++++++++++-------- pkg/backy/ssh.go | 119 +++- pkg/backy/templates/error.txt | 2 +- pkg/backy/templates/success.txt | 2 +- pkg/backy/types.go | 48 +- pkg/backy/utils.go | 77 ++- pkg/configfetcher/configfetcher.go | 24 + pkg/configfetcher/http.go | 31 + pkg/configfetcher/local.go | 26 + pkg/configfetcher/s3.go | 66 +++ pkg/pkgman/apt/apt.go | 40 ++ pkg/pkgman/dnf/dnf.go | 46 ++ pkg/pkgman/pkgcommon/options.go | 13 + pkg/pkgman/pkgman.go | 5 +- pkg/pkgman/yum/yum.go | 38 ++ pkg/usermanager/linux/linux.go | 90 +++ pkg/usermanager/userman.go | 35 ++ 34 files changed, 1072 insertions(+), 353 deletions(-) create mode 100644 .changes/unreleased/Added-20250111-211546.yaml create mode 100644 .changes/unreleased/Added-20250111-211813.yaml create mode 100644 .changes/unreleased/Added-20250113-231248.yaml create mode 100644 .changes/unreleased/Changed-20250113-231007.yaml create mode 100644 .changes/unreleased/Changed-20250113-231622.yaml create mode 100644 pkg/configfetcher/configfetcher.go create mode 100644 pkg/configfetcher/http.go create mode 100644 pkg/configfetcher/local.go create mode 100644 pkg/configfetcher/s3.go create mode 100644 pkg/usermanager/linux/linux.go create mode 100644 pkg/usermanager/userman.go diff --git a/.changes/unreleased/Added-20250111-211546.yaml b/.changes/unreleased/Added-20250111-211546.yaml new file mode 100644 index 0000000..29dd0be --- /dev/null +++ b/.changes/unreleased/Added-20250111-211546.yaml @@ -0,0 +1,3 @@ +kind: Added +body: '[feat]: package `packageOperation` option `checkVersion` implemented' +time: 2025-01-11T21:15:46.207199643-06:00 diff --git a/.changes/unreleased/Added-20250111-211813.yaml b/.changes/unreleased/Added-20250111-211813.yaml new file mode 100644 index 0000000..7810b94 --- /dev/null +++ b/.changes/unreleased/Added-20250111-211813.yaml @@ -0,0 +1,3 @@ +kind: Added +body: user management added - see docs +time: 2025-01-11T21:18:13.182822019-06:00 diff --git a/.changes/unreleased/Added-20250113-231248.yaml b/.changes/unreleased/Added-20250113-231248.yaml new file mode 100644 index 0000000..34a5f78 --- /dev/null +++ b/.changes/unreleased/Added-20250113-231248.yaml @@ -0,0 +1,3 @@ +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 diff --git a/.changes/unreleased/Changed-20250113-231007.yaml b/.changes/unreleased/Changed-20250113-231007.yaml new file mode 100644 index 0000000..af28483 --- /dev/null +++ b/.changes/unreleased/Changed-20250113-231007.yaml @@ -0,0 +1,3 @@ +kind: Changed +body: Internal refactoring of config setup +time: 2025-01-13T23:10:07.215735108-06:00 diff --git a/.changes/unreleased/Changed-20250113-231622.yaml b/.changes/unreleased/Changed-20250113-231622.yaml new file mode 100644 index 0000000..21f33e9 --- /dev/null +++ b/.changes/unreleased/Changed-20250113-231622.yaml @@ -0,0 +1,3 @@ +kind: Changed +body: Formatting and sending for notifications +time: 2025-01-13T23:16:22.260458782-06:00 diff --git a/.changes/v0.6.1.md b/.changes/v0.6.1.md index bdca15d..2e7ed39 100644 --- a/.changes/v0.6.1.md +++ b/.changes/v0.6.1.md @@ -1,3 +1,3 @@ ## v0.6.1 - 2025-01-04 ### Fixed -* Hooks now run explicitly after the command executes. Fixed panic due to improper logic. +* When running a list, hooks now run explicitly after the command executes. Fixed panic due to improper logic. \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 9d8b418..dc52063 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "cSpell.words": [ "Cmds", + "configfetcher", "knadh", "koanf", "mattn", diff --git a/cmd/backup.go b/cmd/backup.go index 056f018..0e73523 100644 --- a/cmd/backup.go +++ b/cmd/backup.go @@ -31,8 +31,7 @@ func init() { func Backup(cmd *cobra.Command, args []string) { backyConfOpts := backy.NewOpts(cfgFile, backy.AddCommandLists(cmdLists)) backyConfOpts.InitConfig() - - backy.ReadConfig(backyConfOpts) + backyConfOpts.ReadConfig() backyConfOpts.RunListConfig("") for _, host := range backyConfOpts.Hosts { diff --git a/cmd/cron.go b/cmd/cron.go index 2f56699..cae3f46 100644 --- a/cmd/cron.go +++ b/cmd/cron.go @@ -19,6 +19,7 @@ func cron(cmd *cobra.Command, args []string) { opts := backy.NewOpts(cfgFile, backy.CronEnabled()) opts.InitConfig() - backy.ReadConfig(opts) + opts.ReadConfig() + opts.Cron() } diff --git a/cmd/exec.go b/cmd/exec.go index 8a2660c..45b8716 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -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)) + opts := backy.NewOpts(cfgFile, backy.AddCommands(args), backy.SetLogFile(logFile)) opts.InitConfig() - // opts.InitMongo() - backy.ReadConfig(opts).ExecuteCmds(opts) + opts.ReadConfig() + opts.ExecuteCmds() } diff --git a/cmd/host.go b/cmd/host.go index e95cbde..036994c 100644 --- a/cmd/host.go +++ b/cmd/host.go @@ -29,10 +29,10 @@ func init() { // 2. stdin (on command line) (TODO) func Host(cmd *cobra.Command, args []string) { - backyConfOpts := backy.NewOpts(cfgFile) + backyConfOpts := backy.NewOpts(cfgFile, backy.SetLogFile(logFile)) backyConfOpts.InitConfig() - backy.ReadConfig(backyConfOpts) + backyConfOpts.ReadConfig() // check CLI input if hostsList == nil { diff --git a/cmd/list.go b/cmd/list.go index b34a358..8936a78 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -31,7 +31,7 @@ func init() { func List(cmd *cobra.Command, args []string) { - // settup based on whats passed in: + // setup based on whats passed in: // - cmds // - lists // - if none, list all commands @@ -42,8 +42,7 @@ func List(cmd *cobra.Command, args []string) { opts := backy.NewOpts(cfgFile) opts.InitConfig() - - opts = backy.ReadConfig(opts) + opts.ReadConfig() opts.ListCommand("rm-sn-db") } diff --git a/cmd/root.go b/cmd/root.go index 77a0ed4..59ba257 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -15,6 +15,7 @@ var ( // Used for flags. cfgFile string verbose bool + logFile string rootCmd = &cobra.Command{ Use: "backy", @@ -33,6 +34,8 @@ 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") diff --git a/examples/backy.yaml b/examples/backy.yaml index e8eabed..97241d4 100644 --- a/examples/backy.yaml +++ b/examples/backy.yaml @@ -28,9 +28,10 @@ commands: cmd: hostname update-docker: type: package - packageManager: apt packageName: docker-ce + packageManager: apt packageVersion: "5:27.4.1-1~debian.12~bookworm" + packageOperation: update cmd-lists: cmds-to-run: # this can be any name you want @@ -67,7 +68,7 @@ hosts: # optional logging: verbose: true - file: /path/to/logs/commands.log + file: ./backy.log console: false cmd-std-out: false diff --git a/go.mod b/go.mod index be69664..380864b 100644 --- a/go.mod +++ b/go.mod @@ -1,29 +1,52 @@ module git.andrewnw.xyz/CyberShell/backy -go 1.20 +go 1.21 + +toolchain go1.22.2 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/file v0.1.0 + github.com/knadh/koanf/providers/rawbytes 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.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 @@ -39,6 +62,7 @@ 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 @@ -64,6 +88,5 @@ require ( 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.v3 v3.0.1 // indirect maunium.net/go/maulogger/v2 v2.4.1 // indirect ) diff --git a/go.sum b/go.sum index cbc5da8..0abdbbb 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,45 @@ 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= @@ -11,6 +52,7 @@ 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= @@ -18,9 +60,12 @@ github.com/go-co-op/gocron v1.33.1/go.mod h1:NLi+bkm4rRSy1F8U7iacZOz0xPseMoIOnva 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.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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= @@ -30,6 +75,7 @@ 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= @@ -51,6 +97,10 @@ 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= @@ -63,12 +113,15 @@ github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY= 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/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/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= @@ -106,6 +159,7 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG 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.10.1-0.20230524175051-ec119421bb97 h1:3RPlVWzZ/PDqmVuf/FKHARG5EMid/tl7cv54Sw/QRVY= +github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 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= @@ -113,6 +167,8 @@ 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= @@ -168,6 +224,7 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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= @@ -181,6 +238,8 @@ 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 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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= diff --git a/pkg/backy/backy.go b/pkg/backy/backy.go index 2f86ec3..e1603df 100644 --- a/pkg/backy/backy.go +++ b/pkg/backy/backy.go @@ -49,64 +49,57 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ ArgsStr += fmt.Sprintf(" %s", v) } - command = getPackageCommand(command) + command = getCommandType(command) + + if command.Type == "user" { + if command.UserOperation == "password" { + cmdCtxLogger.Info().Str("password", command.UserPassword).Msg("user password to be updated") + } + } var errSSH error // is host defined if command.Host != nil { + print("host is defined") outputArr, errSSH = command.RunCmdSSH(cmdCtxLogger, opts) if errSSH != nil { return outputArr, errSSH } } 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 policy: %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) - if command.Dir != nil { - localCMD.Dir = *command.Dir - } - injectEnvIntoLocalCMD(envVars, localCMD, cmdCtxLogger) + } else { - cmdOutWriters = io.MultiWriter(&cmdOutBuf) + cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s on local machine", command.Name)).Send() - 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 + localCMD = exec.Command(command.Cmd, command.Args...) } - cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s on local machine", command.Name)).Send() - - localCMD := exec.Command(command.Cmd, command.Args...) - if command.Dir != nil { localCMD.Dir = *command.Dir } @@ -134,7 +127,9 @@ 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() @@ -173,7 +168,7 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<- // Notify failure if list.NotifyConfig != nil { - notifyError(cmdLogger, msgTemps, list, cmdsRan, outStructArr, runErr, cmdToRun, opts) + notifyError(cmdLogger, msgTemps, list, cmdsRan, outStructArr, runErr, cmdToRun) } hasError = true break @@ -227,7 +222,7 @@ func cmdsRanContains(cmd string, cmdsRan []string) bool { } // Helper to notify errors -func notifyError(logger zerolog.Logger, templates *msgTemplates, list *CmdList, cmdsRan []string, outStructArr []outStruct, err error, cmd *Command, opts *ConfigOpts) { +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, @@ -299,13 +294,7 @@ func (opts *ConfigOpts) RunListConfig(cron string) { opts.closeHostConnections() } -type CmdResult struct { - CmdName string // Name of the command executed - ListName string // Name of the command list - Error error // Error encountered, if any -} - -func (config *ConfigOpts) ExecuteCmds(opts *ConfigOpts) { +func (opts *ConfigOpts) ExecuteCmds() { for _, cmd := range opts.executeCmds { cmdToRun := opts.Cmds[cmd] cmdLogger := cmdToRun.GenerateLogger(opts) @@ -425,3 +414,14 @@ 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 { + +// } +// } diff --git a/pkg/backy/config.go b/pkg/backy/config.go index 140c139..e2fe632 100644 --- a/pkg/backy/config.go +++ b/pkg/backy/config.go @@ -2,17 +2,19 @@ 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/file" + "github.com/knadh/koanf/providers/rawbytes" "github.com/knadh/koanf/v2" "github.com/mattn/go-isatty" "github.com/rs/zerolog" @@ -26,64 +28,72 @@ var configFiles []string const macroStart string = "%{" const macroEnd string = "}%" const envMacroStart string = "%{env:" -const vaultMacroStart string = "%{env:" +const vaultMacroStart string = "%{vault:" func (opts *ConfigOpts) InitConfig() { - - homeDir, homeDirErr = os.UserHomeDir() - - if homeDirErr != nil { - fmt.Println(homeDirErr) - logging.ExitWithMSG(homeDirErr.Error(), 1, nil) + homeDir, err := os.UserHomeDir() + if err != nil { + logging.ExitWithMSG(err.Error(), 1, nil) } - backyHomeConfDir = homeDir + "/.config/backy/" - - configFiles = []string{"./backy.yml", "./backy.yaml", backyHomeConfDir + "backy.yml", backyHomeConfDir + "backy.yaml"} + backyHomeConfDir := path.Join(homeDir, ".config/backy/") + configFiles := []string{ + "./backy.yml", "./backy.yaml", + path.Join(backyHomeConfDir, "backy.yml"), + path.Join(backyHomeConfDir, "backy.yaml"), + } backyKoanf := koanf.New(".") - opts.ConfigFilePath = strings.TrimSpace(opts.ConfigFilePath) + // Initialize the fetcher + fetcher := configfetcher.NewConfigFetcher(opts.ConfigFilePath) + if opts.ConfigFilePath != "" { - err := testFile(opts.ConfigFilePath) - if err != nil { - logging.ExitWithMSG(fmt.Sprintf("Could not open config file %s: %v", opts.ConfigFilePath, err), 1, nil) - } - - if err := backyKoanf.Load(file.Provider(opts.ConfigFilePath), yaml.Parser()); err != nil { - logging.ExitWithMSG(fmt.Sprintf("error loading config: %v", err), 1, &opts.Logger) - } + loadConfigFile(fetcher, opts.ConfigFilePath, backyKoanf, opts) } else { - - 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) - } + loadDefaultConfigFiles(fetcher, configFiles, backyKoanf, opts) } opts.koanf = backyKoanf } -// ReadConfig validates and reads the config file. -func ReadConfig(opts *ConfigOpts) *ConfigOpts { - - 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 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) } + if err := k.Load(rawbytes.Provider(data), yaml.Parser()); err != nil { + logging.ExitWithMSG(fmt.Sprintf("error loading config: %v", err), 1, &opts.Logger) + } +} + +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 opts.loadEnv() @@ -94,228 +104,39 @@ func ReadConfig(opts *ConfigOpts) *ConfigOpts { CheckConfigValues(backyKoanf, opts.ConfigFilePath) - // 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) - } - } + validateCommands(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() + setLoggingOptions(backyKoanf, opts) + log := setupLogger(opts) opts.Logger = log log.Info().Str("config file", opts.ConfigFilePath).Send() - unmarshalErr := backyKoanf.UnmarshalWithConf("commands", &opts.Cmds, koanf.UnmarshalConf{Tag: "yaml"}) + unmarshalConfig(backyKoanf, "commands", &opts.Cmds, opts.Logger) - if unmarshalErr != nil { + validateCommandEnvironments(opts) - panic(fmt.Errorf("error unmarshaling cmds struct: %w", unmarshalErr)) + unmarshalConfig(backyKoanf, "hosts", &opts.Hosts, opts.Logger) - } + resolveHostConfigs(opts) - 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) - } + loadCommandLists(opts, backyKoanf) - expandEnvVars(opts.backyEnv, cmdConf.Environment) - } + validateCommandLists(opts) - // 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) { + 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) } - if len(opts.executeLists) > 0 { - for l := range opts.CmdConfigLists { - if !contains(opts.executeLists, l) { - delete(opts.CmdConfigLists, l) - } - } - } + filterExecuteLists(opts) if backyKoanf.Exists("notifications") { - - unmarshalErr = backyKoanf.UnmarshalWithConf("notifications", &opts.NotificationConf, koanf.UnmarshalConf{Tag: "yaml"}) - if unmarshalErr != nil { - fmt.Printf("error unmarshalling notifications object: %v", unmarshalErr) - } + unmarshalConfig(backyKoanf, "notifications", &opts.NotificationConf, opts.Logger) } opts.SetupNotify() @@ -327,6 +148,176 @@ func ReadConfig(opts *ConfigOpts) *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 := configfetcher.NewConfigFetcher(filePath) + + 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 := configfetcher.NewConfigFetcher(opts.CmdListFile) + + 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) } @@ -448,6 +439,7 @@ func GetVaultKey(str string, opts *ConfigOpts, log zerolog.Logger) string { } func processCmds(opts *ConfigOpts) error { + // process commands for cmdName, cmd := range opts.Cmds { @@ -503,7 +495,7 @@ func processCmds(opts *ConfigOpts) error { // Validate the operation switch cmd.PackageOperation { - case "install", "remove", "upgrade": + case "install", "remove", "upgrade", "checkVersion": cmd.pkgMan, err = pkgman.PackageManagerFactory(cmd.PackageManager, pkgman.WithoutAuth()) if err != nil { return err @@ -511,6 +503,33 @@ 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) + } } } @@ -557,3 +576,32 @@ 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 +} diff --git a/pkg/backy/ssh.go b/pkg/backy/ssh.go index 28508b3..ffea6a0 100644 --- a/pkg/backy/ssh.go +++ b/pkg/backy/ssh.go @@ -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 { @@ -494,7 +494,7 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ) command.Type = strings.TrimSpace(command.Type) - command = getPackageCommand(command) + command = getCommandType(command) // Prepare command arguments for _, v := range command.Args { @@ -516,7 +516,7 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) } // Create new SSH session - commandSession, err := command.createSSHSession(opts) + commandSession, err := command.RemoteHost.createSSHSession(opts) if err != nil { return nil, fmt.Errorf("failed to create SSH session: %w", err) } @@ -539,6 +539,27 @@ 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) @@ -548,11 +569,35 @@ 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), fmt.Errorf("error running command: %w", err) + return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.GetOutput), fmt.Errorf("error running command: %w", err) } } - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger), nil + 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) } // getCommandTypeLabel returns a human-readable label for the command type. @@ -563,20 +608,6 @@ 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() @@ -590,10 +621,10 @@ func (command *Command) runScript(session *ssh.Session, cmdCtxLogger zerolog.Log } if err := session.Wait(); err != nil { - return collectOutput(outputBuf, command.Name, cmdCtxLogger), fmt.Errorf("error waiting for shell: %w", err) + return collectOutput(outputBuf, command.Name, cmdCtxLogger, true), fmt.Errorf("error waiting for shell: %w", err) } - return collectOutput(outputBuf, command.Name, cmdCtxLogger), nil + return collectOutput(outputBuf, command.Name, cmdCtxLogger, command.GetOutput), nil } // runScriptFile handles the execution of script files. @@ -609,10 +640,10 @@ func (command *Command) runScriptFile(session *ssh.Session, cmdCtxLogger zerolog } if err := session.Wait(); err != nil { - return collectOutput(outputBuf, command.Name, cmdCtxLogger), fmt.Errorf("error waiting for shell: %w", err) + return collectOutput(outputBuf, command.Name, cmdCtxLogger, true), fmt.Errorf("error waiting for shell: %w", err) } - return collectOutput(outputBuf, command.Name, cmdCtxLogger), nil + return collectOutput(outputBuf, command.Name, cmdCtxLogger, true), nil } // prepareScriptBuffer prepares a buffer for inline scripts. @@ -677,13 +708,51 @@ 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) []string { +func collectOutput(buf *bytes.Buffer, commandName string, logger zerolog.Logger, wantOutput bool) []string { var outputArr []string scanner := bufio.NewScanner(buf) for scanner.Scan() { line := scanner.Text() outputArr = append(outputArr, line) - logger.Info().Str("cmd", commandName).Str("output", line).Send() + 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 +} diff --git a/pkg/backy/templates/error.txt b/pkg/backy/templates/error.txt index a788b7c..d43c637 100644 --- a/pkg/backy/templates/error.txt +++ b/pkg/backy/templates/error.txt @@ -15,7 +15,7 @@ The following commands ran: {{end}} {{ end }} -{{ if .CmdOutput }}{{- range .CmdOutput }}Command output for {{ .CmdName }}: +{{ if .CmdOutput }}{{- range .CmdOutput }}{{ printf "\n"}}Command output for {{ .CmdName }}: {{- range .Output}} {{ . }} {{ end }}{{ end }} diff --git a/pkg/backy/templates/success.txt b/pkg/backy/templates/success.txt index 262d411..bfeb34d 100644 --- a/pkg/backy/templates/success.txt +++ b/pkg/backy/templates/success.txt @@ -5,7 +5,7 @@ The following commands ran: - {{. -}} {{end}} -{{ if .CmdOutput }}{{- range .CmdOutput }}Command output for {{ .CmdName }}: +{{ if .CmdOutput }}{{- range .CmdOutput }}{{ printf "\n"}}Command output for {{ .CmdName }}: {{- range .Output}} {{ . }} {{ end }}{{ end }} diff --git a/pkg/backy/types.go b/pkg/backy/types.go index cbdccd3..97451ed 100644 --- a/pkg/backy/types.go +++ b/pkg/backy/types.go @@ -4,7 +4,10 @@ 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" @@ -18,6 +21,7 @@ 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"` @@ -114,20 +118,32 @@ type ( // Username specifies the username for user creation or related operations Username string `yaml:"username,omitempty"` - // Groups specifies the groups to add the user to - Groups []string `yaml:"groups,omitempty"` + // UserGroups specifies the groups to add the user to + UserGroups []string `yaml:"userGroups,omitempty"` - // Home specifies the home directory for the user - Home string `yaml:"home,omitempty"` + // UserHome specifies the home directory for the user + UserHome string `yaml:"userHome,omitempty"` - // System specifies whether the user is a system account - System bool `yaml:"system,omitempty"` + // UserShell specifies the shell for the user + UserShell string `yaml:"userShell,omitempty"` - // Password specifies the password for the user (can be file: or plain text) - Password string `yaml:"password,omitempty"` + // SystemUser specifies whether the user is a system account + SystemUser bool `yaml:"systemUser,omitempty"` - // Operation specifies the action for user-related commands (e.g., "create" or "remove") - Operation string `yaml:"operation,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 } RemoteSource struct { @@ -176,6 +192,9 @@ type ( // Holds config file ConfigFilePath string + // Holds log file + LogFilePath string + // for command list file CmdListFile string @@ -252,10 +271,9 @@ type ( Final []string `yaml:"final,omitempty"` } - CmdListResults struct { - // name of the list - ListName string - // command that caused the list to fail - ErrCmd string + CmdResult struct { + CmdName string // Name of the command executed + ListName string // Name of the command list + Error error // Error encountered, if any } ) diff --git a/pkg/backy/utils.go b/pkg/backy/utils.go index e2a47aa..936450b 100644 --- a/pkg/backy/utils.go +++ b/pkg/backy/utils.go @@ -5,6 +5,7 @@ package backy import ( + "bytes" "errors" "fmt" "os" @@ -56,6 +57,13 @@ 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) { @@ -234,9 +242,10 @@ func expandEnvVars(backyEnv map[string]string, envVars []string) { } } -// 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 { +// 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 { if command.Type == "package" && !command.packageCmdSet { command.packageCmdSet = true @@ -247,9 +256,71 @@ func getPackageCommand(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 +} diff --git a/pkg/configfetcher/configfetcher.go b/pkg/configfetcher/configfetcher.go new file mode 100644 index 0000000..a4c5747 --- /dev/null +++ b/pkg/configfetcher/configfetcher.go @@ -0,0 +1,24 @@ +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) ConfigFetcher { + if strings.HasPrefix(source, "http") || strings.HasPrefix(source, "https") { + return &HTTPFetcher{} + } else if strings.HasPrefix(source, "s3") { + return &S3Fetcher{} + } else { + return &LocalFetcher{} + } + +} diff --git a/pkg/configfetcher/http.go b/pkg/configfetcher/http.go new file mode 100644 index 0000000..37542a9 --- /dev/null +++ b/pkg/configfetcher/http.go @@ -0,0 +1,31 @@ +package configfetcher + +import ( + "errors" + "io" + "net/http" + + "gopkg.in/yaml.v3" +) + +type HTTPFetcher struct{} + +// 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) +} diff --git a/pkg/configfetcher/local.go b/pkg/configfetcher/local.go new file mode 100644 index 0000000..c141fa5 --- /dev/null +++ b/pkg/configfetcher/local.go @@ -0,0 +1,26 @@ +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) +} diff --git a/pkg/configfetcher/s3.go b/pkg/configfetcher/s3.go new file mode 100644 index 0000000..e8d8f78 --- /dev/null +++ b/pkg/configfetcher/s3.go @@ -0,0 +1,66 @@ +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 an initialized S3 client +func NewS3Fetcher() (*S3Fetcher, error) { + cfg, err := config.LoadDefaultConfig(context.TODO()) + if err != nil { + return nil, err + } + client := s3.NewFromConfig(cfg) + return &S3Fetcher{S3Client: client}, 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 +} diff --git a/pkg/pkgman/apt/apt.go b/pkg/pkgman/apt/apt.go index 09c211f..d156c7c 100644 --- a/pkg/pkgman/apt/apt.go +++ b/pkg/pkgman/apt/apt.go @@ -2,6 +2,8 @@ package apt import ( "fmt" + "regexp" + "strings" "git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/pkgcommon" ) @@ -10,6 +12,7 @@ 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. @@ -62,6 +65,14 @@ 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) @@ -93,3 +104,32 @@ func (a *AptManager) SetUseAuth(useAuth bool) { func (a *AptManager) SetAuthCommand(authCommand string) { a.authCommand = authCommand } + +// SetPackageParser assigns a PackageParser. +func (a *AptManager) SetPackageParser(parser pkgcommon.PackageParser) { + a.Parser = parser +} + +// 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 +} diff --git a/pkg/pkgman/dnf/dnf.go b/pkg/pkgman/dnf/dnf.go index 9f6e1a1..1d17f7f 100644 --- a/pkg/pkgman/dnf/dnf.go +++ b/pkg/pkgman/dnf/dnf.go @@ -2,6 +2,8 @@ package dnf import ( "fmt" + "regexp" + "strings" "git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/pkgcommon" ) @@ -74,6 +76,50 @@ 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 { diff --git a/pkg/pkgman/pkgcommon/options.go b/pkg/pkgman/pkgcommon/options.go index c450f77..91cf411 100644 --- a/pkg/pkgman/pkgcommon/options.go +++ b/pkg/pkgman/pkgcommon/options.go @@ -2,3 +2,16 @@ 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 +} diff --git a/pkg/pkgman/pkgman.go b/pkg/pkgman/pkgman.go index e037ed2..de3f3e4 100644 --- a/pkg/pkgman/pkgman.go +++ b/pkg/pkgman/pkgman.go @@ -15,7 +15,8 @@ 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) } @@ -67,6 +68,8 @@ 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) } diff --git a/pkg/pkgman/yum/yum.go b/pkg/pkgman/yum/yum.go index a603a2f..50f88e9 100644 --- a/pkg/pkgman/yum/yum.go +++ b/pkg/pkgman/yum/yum.go @@ -2,6 +2,7 @@ package yum import ( "fmt" + "regexp" "git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/pkgcommon" ) @@ -74,6 +75,43 @@ 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 { diff --git a/pkg/usermanager/linux/linux.go b/pkg/usermanager/linux/linux.go new file mode 100644 index 0000000..d0826ac --- /dev/null +++ b/pkg/usermanager/linux/linux.go @@ -0,0 +1,90 @@ +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} +} diff --git a/pkg/usermanager/userman.go b/pkg/usermanager/userman.go new file mode 100644 index 0000000..9532d28 --- /dev/null +++ b/pkg/usermanager/userman.go @@ -0,0 +1,35 @@ +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 + +}