diff --git a/.vscode/settings.json b/.vscode/settings.json index 2ebd360..d2d0aa1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,9 @@ { "cSpell.words": [ - "Cmds" + "Cmds", + "knadh", + "koanf", + "mattn", + "Strs" ] } \ No newline at end of file diff --git a/.woodpecker/gitea.yml b/.woodpecker/gitea.yml index 74fe0a8..fbd74a5 100644 --- a/.woodpecker/gitea.yml +++ b/.woodpecker/gitea.yml @@ -7,4 +7,6 @@ steps: when: event: tag -branches: master \ No newline at end of file +when: + - event: tag + branch: master \ No newline at end of file diff --git a/.woodpecker/publish-docs.yml b/.woodpecker/publish-docs.yml index 4a63629..6fbf4f8 100644 --- a/.woodpecker/publish-docs.yml +++ b/.woodpecker/publish-docs.yml @@ -10,7 +10,6 @@ steps: image: codingkoopa/git-rsync-openssh commands: - cd docs - - echo "151.101.210.132 deb.debian.org" >> /etc/hosts - echo "nameserver 1.1.1.1" > /etc/resolv.conf - mkdir ~/.ssh && chmod -R 700 ~/.ssh # - apt update -y && apt install openssh-client rsync -y @@ -25,6 +24,7 @@ steps: secrets: [ ssh_host_key, ssh_deploy_key, ssh_passphrase ] -branches: master when: - path: "docs/*" \ No newline at end of file + - event: push + branch: master + path: "docs/*" \ No newline at end of file diff --git a/cmd/list.go b/cmd/list.go index 0dbf345..b34a358 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -20,20 +20,30 @@ var ( ) var listsToList []string +var cmdsToList []string func init() { listCmd.Flags().StringSliceVarP(&listsToList, "lists", "l", nil, "Accepts comma-separated names of command lists to list.") + listCmd.Flags().StringSliceVarP(&cmdsToList, "cmds", "c", nil, "Accepts comma-separated names of commands to list.") } func List(cmd *cobra.Command, args []string) { - opts := backy.NewOpts(cfgFile, backy.SetListsToSearch(cmdLists)) + // settup based on whats passed in: + // - cmds + // - lists + // - if none, list all commands + if cmdLists != nil { + + } + + opts := backy.NewOpts(cfgFile) opts.InitConfig() opts = backy.ReadConfig(opts) - opts.ListConfiguration() + opts.ListCommand("rm-sn-db") } diff --git a/docs/content/getting-started/config.md b/docs/content/getting-started/config.md index 73080d4..8710d49 100644 --- a/docs/content/getting-started/config.md +++ b/docs/content/getting-started/config.md @@ -48,7 +48,7 @@ commands: To execute groups of commands in sequence, use a list configuration. ```yaml -cmd-configs: +cmd-lists: cmds-to-run: # this can be any name you want # all commands have to be defined in the commands section order: @@ -97,7 +97,7 @@ hosts: The notifications object can have two forms. -For more, [see the notification object documentation](/config/notifications). The top-level map key is id that has to be referenced by the `cmd-configs` key `notifications`. +For more, [see the notification object documentation](/config/notifications). The top-level map key is id that has to be referenced by the `cmd-lists` key `notifications`. ```yaml notifications: diff --git a/examples/backy.yaml b/examples/backy.yaml index 7a5f61e..b8730a4 100644 --- a/examples/backy.yaml +++ b/examples/backy.yaml @@ -22,7 +22,7 @@ commands: hostname: cmd: hostname -cmd-configs: +cmd-lists: cmds-to-run: # this can be any name you want # all commands have to be defined order: diff --git a/pkg/backy/backy.go b/pkg/backy/backy.go index 59947bd..e6748e7 100644 --- a/pkg/backy/backy.go +++ b/pkg/backy/backy.go @@ -17,7 +17,6 @@ import ( "embed" "github.com/rs/zerolog" - "github.com/rs/zerolog/log" ) //go:embed templates/*.txt @@ -49,6 +48,7 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ ArgsStr += fmt.Sprintf(" %s", v) } + // is host defined if command.Host != nil { command.Type = strings.TrimSpace(command.Type) @@ -64,6 +64,8 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ return nil, err } } + + // create new ssh session commandSession, err := command.RemoteHost.SshClient.NewSession() // Retry connecting to host; if that fails, error. If it does not fail, try to create new session @@ -86,6 +88,7 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ for _, a := range command.Args { cmd += " " + a } + cmdOutWriters = io.MultiWriter(&cmdOutBuf) if IsCmdStdOutEnabled() { @@ -94,12 +97,13 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ commandSession.Stdout = cmdOutWriters commandSession.Stderr = cmdOutWriters + // Is command type defined. That is, is it local or not if command.Type != "" { - // did the program panic while writing to the buffer? defer func() { + // did the program panic while writing to the buffer? if err := recover(); err != nil { - cmdCtxLogger.Info().Msg(fmt.Sprintf("panic occured writing to buffer: %x", err)) + cmdCtxLogger.Info().Msg(fmt.Sprintf("panic occurred writing to buffer: %x", err)) } }() @@ -133,11 +137,13 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ if str, ok := outMap["output"].(string); ok { outputArr = append(outputArr, str) } - log.Info().Fields(outMap).Send() + cmdCtxLogger.Info().Fields(outMap).Send() } return outputArr, nil } + if command.Type == "scriptFile" { + var ( buffer bytes.Buffer scriptEnvFileBuffer bytes.Buffer @@ -164,20 +170,27 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ } command.Cmd, dirErr = resolveDir(command.Cmd) + if dirErr != nil { return nil, dirErr } + + // treat command.Cmd as a file file, err := os.Open(command.Cmd) + if err != nil { return nil, err } + _, err = io.Copy(&scriptFileBuffer, file) + if err != nil { return nil, err } defer file.Close() + // append scriptEnvFile to scriptFileBuffer if scriptEnvFilePresent { _, err := buffer.WriteString(scriptEnvFileBuffer.String()) if err != nil { @@ -283,7 +296,7 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ if str, ok := outMap["output"].(string); ok { outputArr = append(outputArr, str) } - log.Info().Fields(outMap).Send() + cmdCtxLogger.Info().Fields(outMap).Send() } if err != nil { @@ -292,25 +305,30 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ } return outputArr, nil } + cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s %s on local machine", command.Cmd, ArgsStr)).Send() localCMD := exec.Command(command.Cmd, command.Args...) + if command.Dir != nil { localCMD.Dir = *command.Dir } - // fmt.Printf("%v\n", envVars.env) injectEnvIntoLocalCMD(envVars, localCMD, cmdCtxLogger) + cmdOutWriters = io.MultiWriter(&cmdOutBuf) - // fmt.Printf("%v\n", localCMD.Environ()) 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.Cmd @@ -329,17 +347,19 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ return outputArr, nil } +// cmdListWorker func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<- string, opts *ConfigOpts) { + // iterate over list to run for list := range jobs { fieldsMap := make(map[string]interface{}) fieldsMap["list"] = list.Name cmdLog := opts.Logger.Info() - var count int - var cmdsRan []string - var outStructArr []outStruct + 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].Cmd @@ -361,6 +381,7 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<- 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: cmd, @@ -374,8 +395,8 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<- } count++ if runOutErr != nil { - var errMsg bytes.Buffer if list.NotifyConfig != nil { + var errMsg bytes.Buffer errStruct := make(map[string]interface{}) errStruct["listName"] = list.Name @@ -410,7 +431,9 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<- cmdsRan = append(cmdsRan, cmd) var successMsg bytes.Buffer - if list.NotifyConfig != nil { + // 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{}) successStruct["listName"] = list.Name diff --git a/pkg/backy/config.go b/pkg/backy/config.go index bace418..301f7e2 100644 --- a/pkg/backy/config.go +++ b/pkg/backy/config.go @@ -25,13 +25,20 @@ var configFiles []string func (opts *ConfigOpts) InitConfig() { homeDir, homeDirErr = os.UserHomeDir() + if homeDirErr != nil { fmt.Println(homeDirErr) + logging.ExitWithMSG(homeDirErr.Error(), 1, nil) } + 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) + if opts.ConfigFilePath != "" { err := testFile(opts.ConfigFilePath) if err != nil { @@ -43,16 +50,16 @@ func (opts *ConfigOpts) InitConfig() { } } else { - cFileFalures := 0 + cFileFailures := 0 for _, c := range configFiles { if err := backyKoanf.Load(file.Provider(c), yaml.Parser()); err != nil { - cFileFalures++ + cFileFailures++ } else { opts.ConfigFilePath = c break } } - if cFileFalures == len(configFiles) { + 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) } } @@ -80,32 +87,43 @@ 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) } } - for _, l := range opts.executeLists { - if !backyKoanf.Exists(getCmdListFromConfig(l)) { - logging.ExitWithMSG(Sprintf("list %s not found", l), 1, nil) - } - } + // 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 ( - verbose bool - logFile string + isLoggingVerbose bool + logFile string ) - verbose = backyKoanf.Bool(getLoggingKeyFromConfig("verbose")) + isLoggingVerbose = backyKoanf.Bool(getLoggingKeyFromConfig("verbose")) + + logFile = fmt.Sprintf("%s/backy.log", path.Dir(opts.ConfigFilePath)) // get full path to logfile - logFile = fmt.Sprintf("%s/backy.log", path.Dir(opts.ConfigFilePath)) if backyKoanf.Exists(getLoggingKeyFromConfig("file")) { logFile = backyKoanf.String(getLoggingKeyFromConfig("file")) } + zerolog.SetGlobalLevel(zerolog.InfoLevel) - if verbose { + if isLoggingVerbose { zerolog.SetGlobalLevel(zerolog.DebugLevel) globalLvl := zerolog.GlobalLevel() os.Setenv("BACKY_LOGLEVEL", Sprintf("%v", globalLvl)) @@ -128,8 +146,11 @@ func ReadConfig(opts *ConfigOpts) *ConfigOpts { log.Info().Str("config file", opts.ConfigFilePath).Send() unmarshalErr := backyKoanf.UnmarshalWithConf("commands", &opts.Cmds, koanf.UnmarshalConf{Tag: "yaml"}) + if unmarshalErr != nil { - panic(fmt.Errorf("error unmarshalling cmds struct: %w", unmarshalErr)) + + panic(fmt.Errorf("error unmarshaling cmds struct: %w", unmarshalErr)) + } for cmdName, cmdConf := range opts.Cmds { @@ -142,6 +163,8 @@ func ReadConfig(opts *ConfigOpts) *ConfigOpts { expandEnvVars(opts.backyEnv, cmdConf.Environment) } + // 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)) @@ -152,43 +175,91 @@ func ReadConfig(opts *ConfigOpts) *ConfigOpts { } if host.ProxyJump != "" { proxyHosts := strings.Split(host.ProxyJump, ",") - if len(proxyHosts) > 1 { - 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) - } + for hostNum, h := range proxyHosts { + if hostNum > 1 { + proxyHost, defined := opts.Hosts[h] + if defined { + host.ProxyHost = append(host.ProxyHost, proxyHost) } 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) - } + 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) } } - } else { - proxyHost, defined := opts.Hosts[proxyHosts[0]] - if defined { - host.ProxyHost = append(host.ProxyHost, proxyHost) - } else { - newProxy := &Host{Host: proxyHosts[0]} - host.ProxyHost = append(host.ProxyHost, newProxy) - } } + } } - if backyKoanf.Exists("cmd-lists") { - unmarshalErr = backyKoanf.UnmarshalWithConf("cmd-lists", &opts.CmdConfigLists, koanf.UnmarshalConf{Tag: "yaml"}) - if unmarshalErr != nil { - logging.ExitWithMSG((fmt.Sprintf("error unmarshalling cmd list struct: %v", unmarshalErr)), 1, &opts.Logger) + // 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 !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("lists config file", opts.CmdListFile).Send() + + } + + } + } var cmdNotFoundSliceErr []error @@ -209,6 +280,7 @@ func ReadConfig(opts *ConfigOpts) *ConfigOpts { } } + // Exit program if command is not found from list if len(cmdNotFoundSliceErr) > 0 { var cmdNotFoundErrorLog = log.Fatal() cmdNotFoundErrorLog.Errs("commands not found", cmdNotFoundSliceErr).Send() diff --git a/pkg/backy/list.go b/pkg/backy/list.go index df6dcd2..0351bc6 100644 --- a/pkg/backy/list.go +++ b/pkg/backy/list.go @@ -1,5 +1,68 @@ package backy -func (opts *ConfigOpts) ListConfiguration() { +import "fmt" + +/* + Command: command [args...] + Host: Local or remote (list the name) + + List: name + Commands: + flags: list commands + if listcommands: (use list command) + Command: command [args...] + Host: Local or remote (list the name) + +*/ + +// ListCommand searches the commands in the file to find one +func (opts *ConfigOpts) ListCommand(cmd string) { + // bool for commands not found + // gets set to false if a command is not found + // set to true if the command is found + var cmdFound bool = false + var cmdInfo *Command + // check commands in file against cmd + for _, cmdInFile := range opts.executeCmds { + print(cmdInFile) + cmdFound = false + + if cmd == cmdInFile { + cmdFound = true + cmdInfo = opts.Cmds[cmd] + break + } + } + + // print the command's information + if cmdFound { + + print("Command: ") + + print(cmdInfo.Cmd) + if len(cmdInfo.Args) >= 0 { + + for _, v := range cmdInfo.Args { + print(" ") // print space between command and args + print(v) // print command arg + } + } + + // is is remote or local + if cmdInfo.Host != nil { + + print("Host: ", cmdInfo.Host) + + } else { + + print("Host: Runs on Local Machine\n\n") + + } + + } else { + + fmt.Printf("Command %s not found. Check spelling.\n", cmd) + + } } diff --git a/pkg/backy/mongo.go b/pkg/backy/mongo.go deleted file mode 100644 index a2962a9..0000000 --- a/pkg/backy/mongo.go +++ /dev/null @@ -1,95 +0,0 @@ -package backy - -import ( - "context" - "errors" - "fmt" - "os" - "time" - - "github.com/joho/godotenv" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" - "go.mongodb.org/mongo-driver/mongo/readpref" -) - -const mongoConfigKey = "global.mongo" - -func (opts *ConfigOpts) InitMongo() { - - if !opts.koanf.Bool(getMongoConfigKey("enabled")) { - return - } - var ( - err error - client *mongo.Client - ) - - // TODO: Get uri and creditials from config - host := opts.koanf.String(getMongoConfigKey("host")) - port := opts.koanf.Int64(getMongoConfigKey("port")) - - ctx, ctxCancel := context.WithTimeout(context.Background(), 10*time.Second) - defer ctxCancel() - client, err = mongo.Connect(ctx, options.Client().ApplyURI(fmt.Sprintf("mongo://%s:%d", host, port))) - if opts.koanf.Bool(getMongoConfigKey("prod")) { - mongoEnvFileSet := opts.koanf.Exists(getMongoConfigKey("env")) - if mongoEnvFileSet { - getMongoConfigFromEnv(opts) - } - auth := options.Credential{} - auth.Password = opts.koanf.String("global.mongo.password") - auth.Username = opts.koanf.String("global.mongo.username") - client, err = mongo.Connect(ctx, options.Client().SetAuth(auth).ApplyURI("mongodb://localhost:27017")) - - } - if err != nil { - opts.Logger.Fatal().Err(err).Send() - } - if err != nil { - opts.Logger.Fatal().Err(err).Send() - } - defer client.Disconnect(ctx) - err = client.Ping(ctx, readpref.Primary()) - if err != nil { - opts.Logger.Fatal().Err(err).Send() - } - databases, err := client.ListDatabaseNames(ctx, bson.M{}) - if err != nil { - opts.Logger.Fatal().Err(err).Send() - } - fmt.Println(databases) - backyDB := client.Database("backy") - backyDB.CreateCollection(context.Background(), "cmds") - backyDB.CreateCollection(context.Background(), "cmd-lists") - backyDB.CreateCollection(context.Background(), "logs") - opts.DB = backyDB -} - -func getMongoConfigFromEnv(opts *ConfigOpts) error { - mongoEnvFile, err := os.Open(opts.koanf.String("global.mongo.env")) - if err != nil { - return err - } - mongoMap, mongoErr := godotenv.Parse(mongoEnvFile) - if mongoErr != nil { - return err - } - mongoPW, mongoPWFound := mongoMap["MONGO_PASSWORD"] - if !mongoPWFound { - return errors.New("MONGO_PASSWORD not set in " + mongoEnvFile.Name()) - } - mongoUser, mongoUserFound := mongoMap["MONGO_USER"] - if !mongoUserFound { - return errors.New("MONGO_PASSWORD not set in " + mongoEnvFile.Name()) - } - opts.koanf.Set(mongoConfigKey+".password", mongoPW) - opts.koanf.Set(mongoConfigKey+".username", mongoUser) - - return nil -} - -func getMongoConfigKey(key string) string { - return fmt.Sprintf("global.mongo.%s", key) -} diff --git a/pkg/backy/notification.go b/pkg/backy/notification.go index fc29e3e..effa34c 100644 --- a/pkg/backy/notification.go +++ b/pkg/backy/notification.go @@ -39,6 +39,7 @@ func (opts *ConfigOpts) SetupNotify() { } for confName, cmdConfig := range opts.CmdConfigLists { + var services []notify.Notifier for _, id := range cmdConfig.Notifications { if !strings.Contains(id, ".") { diff --git a/pkg/backy/ssh.go b/pkg/backy/ssh.go index ad33035..c6e7298 100644 --- a/pkg/backy/ssh.go +++ b/pkg/backy/ssh.go @@ -20,7 +20,7 @@ import ( "golang.org/x/crypto/ssh/knownhosts" ) -var PrivateKeyExtraInfoErr = errors.New("Private key may be encrypted. \nIf encrypted, make sure the password is specified correctly in the correct section: \n privatekeypassword: env:PR_KEY_PASS \n privatekeypassword: file:/path/to/password-file \n privatekeypassword: password (not recommended). \n ") +var PrivateKeyExtraInfoErr = errors.New("Private key may be encrypted. \nIf encrypted, make sure the password is specified correctly in the correct section. This may be done in one of three ways: \n privatekeypassword: env:PR_KEY_PASS \n privatekeypassword: file:/path/to/password-file \n privatekeypassword: password (not recommended). \n ") var TS = strings.TrimSpace // ConnectToSSHHost connects to a host by looking up the config values in the directory ~/.ssh/config @@ -30,22 +30,26 @@ var TS = strings.TrimSpace // If any value is not found, defaults are used func (remoteConfig *Host) ConnectToSSHHost(opts *ConfigOpts) error { - // var sshClient *ssh.Client var connectErr error if TS(remoteConfig.ConfigFilePath) == "" { remoteConfig.useDefaultConfig = true } - khPath, khPathErr := GetKnownHosts(remoteConfig.KnownHostsFile) + + khPathErr := remoteConfig.GetKnownHosts() if khPathErr != nil { return khPathErr } + if remoteConfig.ClientConfig == nil { remoteConfig.ClientConfig = &ssh.ClientConfig{} } + var configFile *os.File + var sshConfigFileOpenErr error + if !remoteConfig.useDefaultConfig { var err error remoteConfig.ConfigFilePath, err = resolveDir(remoteConfig.ConfigFilePath) @@ -72,9 +76,11 @@ func (remoteConfig *Host) ConnectToSSHHost(opts *ConfigOpts) error { } err := remoteConfig.GetProxyJumpFromConfig(opts.Hosts) + if err != nil { return err } + if remoteConfig.ProxyHost != nil { for _, proxyHost := range remoteConfig.ProxyHost { err := proxyHost.GetProxyJumpConfig(opts.Hosts, opts) @@ -86,10 +92,15 @@ func (remoteConfig *Host) ConnectToSSHHost(opts *ConfigOpts) error { } remoteConfig.ClientConfig.Timeout = time.Second * 30 + remoteConfig.GetPrivateKeyFileFromConfig() + remoteConfig.GetPort() + remoteConfig.GetHostName() + remoteConfig.CombineHostNameWithPort() + remoteConfig.GetSshUserFromConfig() if remoteConfig.HostName == "" { @@ -101,7 +112,7 @@ func (remoteConfig *Host) ConnectToSSHHost(opts *ConfigOpts) error { return err } - hostKeyCallback, err := knownhosts.New(khPath) + hostKeyCallback, err := knownhosts.New(remoteConfig.KnownHostsFile) if err != nil { return errors.Wrap(err, "could not create hostkeycallback function") } @@ -128,12 +139,19 @@ func (remoteConfig *Host) ConnectToSSHHost(opts *ConfigOpts) error { } func (remoteHost *Host) GetSshUserFromConfig() { + if TS(remoteHost.User) == "" { + remoteHost.User, _ = remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "User") + if TS(remoteHost.User) == "" { + remoteHost.User = remoteHost.SSHConfigFile.DefaultUserSettings.Get(remoteHost.Host, "User") + if TS(remoteHost.User) == "" { + currentUser, _ := user.Current() + remoteHost.User = currentUser.Username } } @@ -145,39 +163,60 @@ func (remoteHost *Host) GetAuthMethods(opts *ConfigOpts) error { var signer ssh.Signer var err error var privateKey []byte + remoteHost.Password = strings.TrimSpace(remoteHost.Password) + remoteHost.PrivateKeyPassword = strings.TrimSpace(remoteHost.PrivateKeyPassword) + remoteHost.PrivateKeyPath = strings.TrimSpace(remoteHost.PrivateKeyPath) + if remoteHost.PrivateKeyPath != "" { + privateKey, err = os.ReadFile(remoteHost.PrivateKeyPath) + if err != nil { return err } + remoteHost.PrivateKeyPassword, err = GetPrivateKeyPassword(remoteHost.PrivateKeyPassword, opts, opts.Logger) + if err != nil { return err } + if remoteHost.PrivateKeyPassword == "" { + signer, err = ssh.ParsePrivateKey(privateKey) + if err != nil { return errors.Errorf("Failed to open private key file %s: %v \n\n %v", remoteHost.PrivateKeyPath, err, PrivateKeyExtraInfoErr) } + remoteHost.ClientConfig.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)} } else { + signer, err = ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(remoteHost.PrivateKeyPassword)) + if err != nil { return errors.Errorf("Failed to open private key file %s: %v \n\n %v", remoteHost.PrivateKeyPath, err, PrivateKeyExtraInfoErr) } + remoteHost.ClientConfig.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)} } } + if remoteHost.Password == "" { + remoteHost.Password, err = GetPassword(remoteHost.Password, opts, opts.Logger) + if err != nil { + return err } + remoteHost.ClientConfig.Auth = append(remoteHost.ClientConfig.Auth, ssh.Password(remoteHost.Password)) } + return nil } @@ -209,10 +248,17 @@ func (remoteHost *Host) GetPrivateKeyFileFromConfig() { func (remoteHost *Host) GetPort() { port := fmt.Sprintf("%d", remoteHost.Port) // port specifed? + // port will be 0 if missing from backy config if port == "0" { + // get port from specified SSH config file port, _ = remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "Port") + if port == "" { + + // get port from default SSH config file port = remoteHost.SSHConfigFile.DefaultUserSettings.Get(remoteHost.Host, "Port") + + // set port to be default if port == "" { port = "22" } @@ -223,10 +269,12 @@ func (remoteHost *Host) GetPort() { } func (remoteHost *Host) CombineHostNameWithPort() { - port := fmt.Sprintf(":%d", remoteHost.Port) - if strings.HasSuffix(remoteHost.HostName, port) { + + // if the port is already in the HostName, leave it + if strings.HasSuffix(remoteHost.HostName, fmt.Sprintf(":%d", remoteHost.Port)) { return } + remoteHost.HostName = fmt.Sprintf("%s:%d", remoteHost.HostName, remoteHost.Port) } @@ -265,18 +313,22 @@ func (remoteHost *Host) ConnectThroughBastion(log zerolog.Logger) (*ssh.Client, return nil, err } - sClient := ssh.NewClient(ncc, chans, reqs) // sClient is an ssh client connected to the service host, through the bastion host. + sClient := ssh.NewClient(ncc, chans, reqs) return sClient, nil } -func GetKnownHosts(khPath string) (string, error) { - - if TS(khPath) != "" { - return resolveDir(khPath) +// GetKnownHosts resolves the host's KnownHosts file if it is defined +// if not defined, the default location for this file is used +func (remotehHost *Host) GetKnownHosts() error { + var knownHostsFileErr error + if TS(remotehHost.KnownHostsFile) != "" { + remotehHost.KnownHostsFile, knownHostsFileErr = resolveDir(remotehHost.KnownHostsFile) + return knownHostsFileErr } - return resolveDir("~/.ssh/known_hosts") + remotehHost.KnownHostsFile, knownHostsFileErr = resolveDir("~/.ssh/known_hosts") + return knownHostsFileErr } func GetPrivateKeyPassword(key string, opts *ConfigOpts, log zerolog.Logger) (string, error) { @@ -306,6 +358,7 @@ func GetPrivateKeyPassword(key string, opts *ConfigOpts, log zerolog.Logger) (st return prKeyPassword, nil } +// GetPassword gets any password func GetPassword(pass string, opts *ConfigOpts, log zerolog.Logger) (string, error) { pass = strings.TrimSpace(pass) @@ -370,7 +423,7 @@ func (remoteConfig *Host) GetProxyJumpConfig(hosts map[string]*Host, opts *Confi remoteConfig.useDefaultConfig = true } - khPath, khPathErr := GetKnownHosts(remoteConfig.KnownHostsFile) + khPathErr := remoteConfig.GetKnownHosts() if khPathErr != nil { return khPathErr @@ -415,7 +468,7 @@ func (remoteConfig *Host) GetProxyJumpConfig(hosts map[string]*Host, opts *Confi } // TODO: Add value/option to config for host key and add bool to check for host key - hostKeyCallback, err := knownhosts.New(khPath) + hostKeyCallback, err := knownhosts.New(remoteConfig.KnownHostsFile) if err != nil { return fmt.Errorf("could not create hostkeycallback function: %v", err) } diff --git a/pkg/backy/types.go b/pkg/backy/types.go index 351c296..1677d63 100644 --- a/pkg/backy/types.go +++ b/pkg/backy/types.go @@ -9,30 +9,10 @@ import ( "github.com/knadh/koanf/v2" "github.com/nikoksr/notify" "github.com/rs/zerolog" - "go.mongodb.org/mongo-driver/bson/primitive" - "go.mongodb.org/mongo-driver/mongo" "golang.org/x/crypto/ssh" ) type ( - CmdConfigSchema struct { - ID primitive.ObjectID `bson:"_id,omitempty"` - CmdList []string `bson:"command-list,omitempty"` - Name string `bson:"name,omitempty"` - } - - CmdSchema struct { - ID primitive.ObjectID `bson:"_id,omitempty"` - Cmd string `bson:"cmd,omitempty"` - Args []string `bson:"args,omitempty"` - Host string `bson:"host,omitempty"` - Dir string `bson:"dir,omitempty"` - } - - Schemas struct { - CmdConfigSchema - CmdSchema - } // Host defines a host to which to connect. // If not provided, the values will be looked up in the default ssh config files @@ -107,12 +87,15 @@ type ( BackyOptionFunc func(*ConfigOpts) CmdList struct { - Name string `yaml:"name,omitempty"` - Cron string `yaml:"cron,omitempty"` - Order []string `yaml:"order,omitempty"` - Notifications []string `yaml:"notifications,omitempty"` - GetOutput bool `yaml:"getOutput,omitempty"` - NotifyConfig *notify.Notify + Name string `yaml:"name,omitempty"` + Cron string `yaml:"cron,omitempty"` + RunCmdOnFailure string `yaml:"runCmdOnFailure,omitempty"` + Order []string `yaml:"order,omitempty"` + Notifications []string `yaml:"notifications,omitempty"` + GetOutput bool `yaml:"getOutput,omitempty"` + NotifyOnSuccess bool `yaml:"notifyOnSuccess,omitempty"` + + NotifyConfig *notify.Notify } ConfigOpts struct { @@ -136,9 +119,9 @@ type ( // Holds config file ConfigFilePath string - Schemas + // for command list file + CmdListFile string - DB *mongo.Database // use command lists using cron useCron bool // Holds commands to execute for the exec command diff --git a/pkg/backy/utils.go b/pkg/backy/utils.go index 8379259..999fae0 100644 --- a/pkg/backy/utils.go +++ b/pkg/backy/utils.go @@ -197,6 +197,7 @@ func resolveDir(path string) (string, error) { return path, nil } +// loadEnv loads a .env file from the config file directory func (opts *ConfigOpts) loadEnv() { envFileInConfigDir := fmt.Sprintf("%s/.env", path.Dir(opts.ConfigFilePath)) var backyEnv map[string]string @@ -208,6 +209,7 @@ func (opts *ConfigOpts) loadEnv() { opts.backyEnv = backyEnv } +// expandEnvVars expands environment variables with the env used in the config func expandEnvVars(backyEnv map[string]string, envVars []string) { env := func(name string) string {