v0.6.0
This commit is contained in:
@ -11,7 +11,6 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"embed"
|
||||
@ -50,257 +49,14 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
|
||||
ArgsStr += fmt.Sprintf(" %s", v)
|
||||
}
|
||||
|
||||
command = getPackageCommand(command)
|
||||
|
||||
var errSSH error
|
||||
// is host defined
|
||||
if command.Host != nil {
|
||||
command.Type = strings.TrimSpace(command.Type)
|
||||
|
||||
if command.Type != "" {
|
||||
cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running script %s on host %s", command.Name, *command.Host)).Send()
|
||||
} else {
|
||||
cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s on host %s", command.Name, *command.Host)).Send()
|
||||
}
|
||||
|
||||
if command.RemoteHost.SshClient == nil {
|
||||
err := command.RemoteHost.ConnectToSSHHost(opts)
|
||||
if err != nil {
|
||||
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
|
||||
if err != nil {
|
||||
connErr := command.RemoteHost.ConnectToSSHHost(opts)
|
||||
if connErr != nil {
|
||||
return nil, fmt.Errorf("error creating session: %v, and error creating new connection to host: %v", err, connErr)
|
||||
}
|
||||
commandSession, err = command.RemoteHost.SshClient.NewSession()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating session: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
defer commandSession.Close()
|
||||
|
||||
injectEnvIntoSSH(envVars, commandSession, opts, cmdCtxLogger)
|
||||
cmd := command.Cmd
|
||||
for _, a := range command.Args {
|
||||
cmd += " " + a
|
||||
}
|
||||
|
||||
cmdOutWriters = io.MultiWriter(&cmdOutBuf)
|
||||
|
||||
if IsCmdStdOutEnabled() {
|
||||
cmdOutWriters = io.MultiWriter(os.Stdout, &cmdOutBuf)
|
||||
}
|
||||
|
||||
commandSession.Stdout = cmdOutWriters
|
||||
commandSession.Stderr = cmdOutWriters
|
||||
// Is command type defined. That is, is it local or not
|
||||
if command.Type != "" {
|
||||
|
||||
var (
|
||||
script *bytes.Buffer
|
||||
buffer bytes.Buffer
|
||||
scriptEnvFileBuffer bytes.Buffer
|
||||
scriptFileBuffer bytes.Buffer
|
||||
dirErr error
|
||||
scriptEnvFilePresent bool
|
||||
)
|
||||
|
||||
defer func() {
|
||||
// did the program panic while writing to the buffer?
|
||||
if err := recover(); err != nil {
|
||||
// cmdCtxLogger.Debug().Msg(fmt.Sprintf("script buffer: %v", script))
|
||||
cmdCtxLogger.Info().Msg(fmt.Sprintf("panic occurred writing to buffer: %v", err))
|
||||
}
|
||||
}()
|
||||
|
||||
if command.Type == "script" {
|
||||
|
||||
if command.ScriptEnvFile != "" {
|
||||
|
||||
command.ScriptEnvFile, dirErr = resolveDir(command.ScriptEnvFile)
|
||||
if dirErr != nil {
|
||||
return nil, dirErr
|
||||
}
|
||||
file, err := os.Open(command.ScriptEnvFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
_, err = io.Copy(&scriptEnvFileBuffer, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Bug: writing to buffer triggers panic
|
||||
// why?
|
||||
// use bytes.Buffer instead of pointer to memory (*bytes.Buffer)
|
||||
|
||||
_, err = buffer.WriteString(scriptEnvFileBuffer.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// write newline
|
||||
buffer.WriteByte(0x0A)
|
||||
|
||||
_, err = buffer.WriteString(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
script = &buffer
|
||||
|
||||
} else {
|
||||
script = bytes.NewBufferString(cmd + "\n")
|
||||
}
|
||||
|
||||
commandSession.Stdin = script
|
||||
if err := commandSession.Shell(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := commandSession.Wait(); err != nil {
|
||||
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()
|
||||
}
|
||||
return outputArr, err
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
return outputArr, nil
|
||||
}
|
||||
|
||||
if command.Type == "scriptFile" {
|
||||
|
||||
if command.ScriptEnvFile != "" {
|
||||
|
||||
command.ScriptEnvFile, dirErr = resolveDir(command.ScriptEnvFile)
|
||||
if dirErr != nil {
|
||||
return nil, dirErr
|
||||
}
|
||||
file, err := os.Open(command.ScriptEnvFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
_, err = io.Copy(&scriptEnvFileBuffer, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scriptEnvFilePresent = true
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
// write newline
|
||||
buffer.WriteByte(0x0A)
|
||||
|
||||
_, err = buffer.WriteString(scriptFileBuffer.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
_, err = io.Copy(&buffer, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
script := &buffer
|
||||
|
||||
commandSession.Stdin = script
|
||||
if err := commandSession.Shell(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := commandSession.Wait(); err != nil {
|
||||
outScanner := bufio.NewScanner(&cmdOutBuf)
|
||||
for outScanner.Scan() {
|
||||
outMap := make(map[string]interface{})
|
||||
outMap["cmd"] = cmd
|
||||
outMap["output"] = outScanner.Text()
|
||||
if str, ok := outMap["output"].(string); ok {
|
||||
outputArr = append(outputArr, str)
|
||||
}
|
||||
cmdCtxLogger.Info().Fields(outMap).Send()
|
||||
}
|
||||
return outputArr, err
|
||||
}
|
||||
|
||||
outScanner := bufio.NewScanner(&cmdOutBuf)
|
||||
for outScanner.Scan() {
|
||||
outMap := make(map[string]interface{})
|
||||
outMap["cmd"] = cmd
|
||||
outMap["output"] = outScanner.Text()
|
||||
if str, ok := outMap["output"].(string); ok {
|
||||
outputArr = append(outputArr, str)
|
||||
}
|
||||
cmdCtxLogger.Info().Fields(outMap).Send()
|
||||
}
|
||||
return outputArr, nil
|
||||
}
|
||||
return nil, fmt.Errorf("command type not recognized")
|
||||
}
|
||||
|
||||
err = commandSession.Run(cmd)
|
||||
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
|
||||
outputArr, errSSH = command.RunCmdSSH(cmdCtxLogger, opts)
|
||||
if errSSH != nil {
|
||||
return outputArr, errSSH
|
||||
}
|
||||
} else {
|
||||
|
||||
@ -408,7 +164,7 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<-
|
||||
fieldsMap["cmd"] = opts.Cmds[cmd].Name
|
||||
cmdToRun := opts.Cmds[cmd]
|
||||
|
||||
cmdLogger = cmdToRun.generateLogger(opts)
|
||||
cmdLogger = cmdToRun.GenerateLogger(opts)
|
||||
cmdLogger.Info().Fields(fieldsMap).Send()
|
||||
|
||||
outputArr, runOutErr := cmdToRun.RunCmd(cmdLogger, opts)
|
||||
@ -555,7 +311,7 @@ func (opts *ConfigOpts) RunListConfig(cron string) {
|
||||
func (config *ConfigOpts) ExecuteCmds(opts *ConfigOpts) {
|
||||
for _, cmd := range opts.executeCmds {
|
||||
cmdToRun := opts.Cmds[cmd]
|
||||
cmdLogger := cmdToRun.generateLogger(opts)
|
||||
cmdLogger := cmdToRun.GenerateLogger(opts)
|
||||
_, runErr := cmdToRun.RunCmd(cmdLogger, opts)
|
||||
if runErr != nil {
|
||||
opts.Logger.Err(runErr).Send()
|
||||
@ -575,7 +331,6 @@ func (config *ConfigOpts) ExecuteCmds(opts *ConfigOpts) {
|
||||
|
||||
func (c *ConfigOpts) closeHostConnections() {
|
||||
for _, host := range c.Hosts {
|
||||
c.Logger.Info().Str("server", host.HostName)
|
||||
if host.isProxyHost {
|
||||
continue
|
||||
}
|
||||
@ -643,16 +398,33 @@ func (cmd *Command) ExecuteHooks(hookType string, opts *ConfigOpts) {
|
||||
}
|
||||
}
|
||||
|
||||
func (cmd *Command) generateLogger(opts *ConfigOpts) zerolog.Logger {
|
||||
func (cmd *Command) GenerateLogger(opts *ConfigOpts) zerolog.Logger {
|
||||
cmdLogger := opts.Logger.With().
|
||||
Str("backy-cmd", cmd.Name).Str("Host", "local machine").
|
||||
Str("Backy-cmd", cmd.Name).Str("Host", "local machine").
|
||||
Logger()
|
||||
|
||||
if cmd.Host != nil {
|
||||
cmdLogger = opts.Logger.With().
|
||||
Str("backy-cmd", cmd.Name).Str("Host", *cmd.Host).
|
||||
Str("Backy-cmd", cmd.Name).Str("Host", *cmd.Host).
|
||||
Logger()
|
||||
|
||||
}
|
||||
return cmdLogger
|
||||
}
|
||||
|
||||
func (opts *ConfigOpts) ExecCmdsSSH(cmdList []string, hostsList []string) {
|
||||
// Iterate over hosts and exec commands
|
||||
for _, h := range hostsList {
|
||||
host := opts.Hosts[h]
|
||||
for _, c := range cmdList {
|
||||
cmd := opts.Cmds[c]
|
||||
cmd.RemoteHost = host
|
||||
cmd.Host = &host.Host
|
||||
opts.Logger.Info().Str("host", h).Str("cmd", c).Send()
|
||||
_, err := cmd.RunCmdSSH(cmd.GenerateLogger(opts), opts)
|
||||
if err != nil {
|
||||
opts.Logger.Err(err).Str("host", h).Str("cmd", c).Send()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"git.andrewnw.xyz/CyberShell/backy/pkg/logging"
|
||||
"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman"
|
||||
vault "github.com/hashicorp/vault/api"
|
||||
"github.com/knadh/koanf/parsers/yaml"
|
||||
"github.com/knadh/koanf/providers/file"
|
||||
@ -486,6 +487,32 @@ func processCmds(opts *ConfigOpts) error {
|
||||
cmd.RemoteHost = &Host{Host: *cmd.Host}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse package commands
|
||||
if cmd.Type == "package" {
|
||||
if cmd.PackageManager == "" {
|
||||
return fmt.Errorf("package manager is required for package command %s", cmd.PackageName)
|
||||
}
|
||||
if cmd.PackageOperation == "" {
|
||||
return fmt.Errorf("package operation is required for package command %s", cmd.PackageName)
|
||||
}
|
||||
if cmd.PackageName == "" {
|
||||
return fmt.Errorf("package name is required for package command %s", cmd.PackageName)
|
||||
}
|
||||
var err error
|
||||
|
||||
// Validate the operation
|
||||
switch cmd.PackageOperation {
|
||||
case "install", "remove", "upgrade":
|
||||
cmd.pkgMan, err = pkgman.PackageManagerFactory(cmd.PackageManager, pkgman.WithoutAuth())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported package operation %s for command %s", cmd.PackageOperation, cmd.Name)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
216
pkg/backy/ssh.go
216
pkg/backy/ssh.go
@ -6,7 +6,9 @@ package backy
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/user"
|
||||
"strconv"
|
||||
@ -23,12 +25,12 @@ import (
|
||||
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
|
||||
// ConnectToHost connects to a host by looking up the config values in the file ~/.ssh/config
|
||||
// It uses any set values and looks up an unset values in the config files
|
||||
// It returns an ssh.Client used to run commands against.
|
||||
// remoteConfig is modified directly. The *ssh.Client is returned as part of remoteConfig,
|
||||
// If configFile is empty, any required configuration is looked up in the default config files
|
||||
// If any value is not found, defaults are used
|
||||
func (remoteConfig *Host) ConnectToSSHHost(opts *ConfigOpts) error {
|
||||
func (remoteConfig *Host) ConnectToHost(opts *ConfigOpts) error {
|
||||
|
||||
var connectErr error
|
||||
|
||||
@ -477,3 +479,211 @@ func (remoteConfig *Host) GetProxyJumpConfig(hosts map[string]*Host, opts *Confi
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunCmdSSH runs commands over SSH.
|
||||
func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([]string, error) {
|
||||
var (
|
||||
ArgsStr string
|
||||
cmdOutBuf bytes.Buffer
|
||||
cmdOutWriters io.Writer
|
||||
|
||||
envVars = environmentVars{
|
||||
file: command.Env,
|
||||
env: command.Environment,
|
||||
}
|
||||
)
|
||||
|
||||
command.Type = strings.TrimSpace(command.Type)
|
||||
command = getPackageCommand(command)
|
||||
|
||||
// Prepare command arguments
|
||||
for _, v := range command.Args {
|
||||
ArgsStr += fmt.Sprintf(" %s", v)
|
||||
}
|
||||
|
||||
cmdCtxLogger.Info().
|
||||
Str("Command", command.Name).
|
||||
Str("Host", *command.Host).
|
||||
Msgf("Running %s on host %s", getCommandTypeLabel(command.Type), *command.Host)
|
||||
|
||||
cmdCtxLogger.Debug().Str("cmd", command.Cmd).Strs("args", command.Args).Send()
|
||||
|
||||
// Ensure SSH client is connected
|
||||
if command.RemoteHost.SshClient == nil {
|
||||
if err := command.RemoteHost.ConnectToHost(opts); err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to host: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create new SSH session
|
||||
commandSession, err := command.createSSHSession(opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create SSH session: %w", err)
|
||||
}
|
||||
defer commandSession.Close()
|
||||
|
||||
// Inject environment variables
|
||||
injectEnvIntoSSH(envVars, commandSession, opts, cmdCtxLogger)
|
||||
|
||||
// Set output writers
|
||||
cmdOutWriters = io.MultiWriter(&cmdOutBuf)
|
||||
if IsCmdStdOutEnabled() {
|
||||
cmdOutWriters = io.MultiWriter(os.Stdout, &cmdOutBuf)
|
||||
}
|
||||
commandSession.Stdout = cmdOutWriters
|
||||
commandSession.Stderr = cmdOutWriters
|
||||
|
||||
// Handle command execution based on type
|
||||
switch command.Type {
|
||||
case "script":
|
||||
return command.runScript(commandSession, cmdCtxLogger, &cmdOutBuf)
|
||||
case "scriptFile":
|
||||
return command.runScriptFile(commandSession, cmdCtxLogger, &cmdOutBuf)
|
||||
default:
|
||||
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), fmt.Errorf("error running command: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger), nil
|
||||
}
|
||||
|
||||
// getCommandTypeLabel returns a human-readable label for the command type.
|
||||
func getCommandTypeLabel(commandType string) string {
|
||||
if commandType == "" {
|
||||
return "command"
|
||||
}
|
||||
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()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
session.Stdin = script
|
||||
|
||||
if err := session.Shell(); err != nil {
|
||||
return nil, fmt.Errorf("error starting shell: %w", err)
|
||||
}
|
||||
|
||||
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), nil
|
||||
}
|
||||
|
||||
// runScriptFile handles the execution of script files.
|
||||
func (command *Command) runScriptFile(session *ssh.Session, cmdCtxLogger zerolog.Logger, outputBuf *bytes.Buffer) ([]string, error) {
|
||||
script, err := command.prepareScriptFileBuffer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
session.Stdin = script
|
||||
|
||||
if err := session.Shell(); err != nil {
|
||||
return nil, fmt.Errorf("error starting shell: %w", err)
|
||||
}
|
||||
|
||||
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), nil
|
||||
}
|
||||
|
||||
// prepareScriptBuffer prepares a buffer for inline scripts.
|
||||
func (command *Command) prepareScriptBuffer() (*bytes.Buffer, error) {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
if command.ScriptEnvFile != "" {
|
||||
envBuffer, err := readFileToBuffer(command.ScriptEnvFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buffer.Write(envBuffer.Bytes())
|
||||
buffer.WriteByte('\n')
|
||||
}
|
||||
|
||||
buffer.WriteString(command.Cmd + "\n")
|
||||
return &buffer, nil
|
||||
}
|
||||
|
||||
// prepareScriptFileBuffer prepares a buffer for script files.
|
||||
func (command *Command) prepareScriptFileBuffer() (*bytes.Buffer, error) {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
// Handle script environment file
|
||||
if command.ScriptEnvFile != "" {
|
||||
envBuffer, err := readFileToBuffer(command.ScriptEnvFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buffer.Write(envBuffer.Bytes())
|
||||
buffer.WriteByte('\n')
|
||||
}
|
||||
|
||||
// Handle script file
|
||||
scriptBuffer, err := readFileToBuffer(command.Cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buffer.Write(scriptBuffer.Bytes())
|
||||
|
||||
return &buffer, nil
|
||||
}
|
||||
|
||||
// readFileToBuffer reads a file into a buffer.
|
||||
func readFileToBuffer(filePath string) (*bytes.Buffer, error) {
|
||||
resolvedPath, err := resolveDir(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file, err := os.Open(resolvedPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var buffer bytes.Buffer
|
||||
if _, err := io.Copy(&buffer, file); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &buffer, nil
|
||||
}
|
||||
|
||||
// collectOutput collects output from a buffer and logs it.
|
||||
func collectOutput(buf *bytes.Buffer, commandName string, logger zerolog.Logger) []string {
|
||||
var outputArr []string
|
||||
scanner := bufio.NewScanner(buf)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
outputArr = append(outputArr, line)
|
||||
logger.Info().Str("cmd", commandName).Str("output", line).Send()
|
||||
}
|
||||
return outputArr
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"text/template"
|
||||
|
||||
"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman"
|
||||
vaultapi "github.com/hashicorp/vault/api"
|
||||
"github.com/kevinburke/ssh_config"
|
||||
"github.com/knadh/koanf/v2"
|
||||
@ -89,6 +90,53 @@ type (
|
||||
GetOutput bool `yaml:"getOutput,omitempty"`
|
||||
|
||||
ScriptEnvFile string `yaml:"scriptEnvFile"`
|
||||
|
||||
PackageManager string `yaml:"packageManager,omitempty"`
|
||||
|
||||
PackageName string `yaml:"packageName,omitempty"`
|
||||
|
||||
// Version specifies the desired version for package execution
|
||||
PackageVersion string `yaml:"packageVersion,omitempty"`
|
||||
|
||||
// PackageOperation specifies the action for package-related commands (e.g., "install" or "remove")
|
||||
PackageOperation string `yaml:"packageOperation,omitempty"`
|
||||
|
||||
pkgMan pkgman.PackageManager
|
||||
|
||||
packageCmdSet bool
|
||||
|
||||
// RemoteSource specifies a URL to fetch the command or configuration remotely
|
||||
RemoteSource string `yaml:"remoteSource,omitempty"`
|
||||
|
||||
// FetchBeforeExecution determines if the remoteSource should be fetched before running
|
||||
FetchBeforeExecution bool `yaml:"fetchBeforeExecution,omitempty"`
|
||||
|
||||
// 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"`
|
||||
|
||||
// Home specifies the home directory for the user
|
||||
Home string `yaml:"home,omitempty"`
|
||||
|
||||
// System specifies whether the user is a system account
|
||||
System bool `yaml:"system,omitempty"`
|
||||
|
||||
// Password specifies the password for the user (can be file: or plain text)
|
||||
Password string `yaml:"password,omitempty"`
|
||||
|
||||
// Operation specifies the action for user-related commands (e.g., "create" or "remove")
|
||||
Operation string `yaml:"operation,omitempty"`
|
||||
}
|
||||
|
||||
RemoteSource struct {
|
||||
URL string `yaml:"url"`
|
||||
Type string `yaml:"type"` // e.g., yaml
|
||||
Auth struct {
|
||||
AccessKey string `yaml:"accessKey"`
|
||||
SecretKey string `yaml:"secretKey"`
|
||||
} `yaml:"auth"`
|
||||
}
|
||||
|
||||
BackyOptionFunc func(*ConfigOpts)
|
||||
@ -103,6 +151,8 @@ type (
|
||||
NotifyOnSuccess bool `yaml:"notifyOnSuccess,omitempty"`
|
||||
|
||||
NotifyConfig *notify.Notify
|
||||
Source string `yaml:"source"` // URL to fetch remote commands
|
||||
Type string `yaml:"type"`
|
||||
}
|
||||
|
||||
ConfigOpts struct {
|
||||
|
@ -233,3 +233,23 @@ 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 {
|
||||
|
||||
if command.Type == "package" && !command.packageCmdSet {
|
||||
command.packageCmdSet = true
|
||||
switch command.PackageOperation {
|
||||
case "install":
|
||||
command.Cmd, command.Args = command.pkgMan.Install(command.PackageName, command.PackageVersion, command.Args)
|
||||
case "remove":
|
||||
command.Cmd, command.Args = command.pkgMan.Remove(command.PackageName, command.Args)
|
||||
case "upgrade":
|
||||
command.Cmd, command.Args = command.pkgMan.Upgrade(command.PackageName, command.PackageVersion)
|
||||
}
|
||||
} else if command.Type != "package" {
|
||||
command.packageCmdSet = false
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
95
pkg/pkgman/apt/apt.go
Normal file
95
pkg/pkgman/apt/apt.go
Normal file
@ -0,0 +1,95 @@
|
||||
package apt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/pkgcommon"
|
||||
)
|
||||
|
||||
// AptManager implements PackageManager for systems using APT.
|
||||
type AptManager struct {
|
||||
useAuth bool // Whether to use an authentication command
|
||||
authCommand string // The authentication command, e.g., "sudo"
|
||||
}
|
||||
|
||||
// DefaultAuthCommand is the default command used for authentication.
|
||||
const DefaultAuthCommand = "sudo"
|
||||
|
||||
const DefaultPackageCommand = "apt-get"
|
||||
|
||||
// NewAptManager creates a new AptManager with default settings.
|
||||
func NewAptManager() *AptManager {
|
||||
return &AptManager{
|
||||
useAuth: true,
|
||||
authCommand: DefaultAuthCommand,
|
||||
}
|
||||
}
|
||||
|
||||
// Install returns the command and arguments for installing a package.
|
||||
func (a *AptManager) Install(pkg, version string, args []string) (string, []string) {
|
||||
baseCmd := a.prependAuthCommand(DefaultPackageCommand)
|
||||
baseArgs := []string{"update", "&&", baseCmd, "install", "-y"}
|
||||
if version != "" {
|
||||
baseArgs = append(baseArgs, fmt.Sprintf("%s=%s", pkg, version))
|
||||
} else {
|
||||
baseArgs = append(baseArgs, pkg)
|
||||
}
|
||||
if args != nil {
|
||||
baseArgs = append(baseArgs, args...)
|
||||
}
|
||||
return baseCmd, baseArgs
|
||||
}
|
||||
|
||||
// Remove returns the command and arguments for removing a package.
|
||||
func (a *AptManager) Remove(pkg string, args []string) (string, []string) {
|
||||
baseCmd := a.prependAuthCommand(DefaultPackageCommand)
|
||||
baseArgs := []string{"remove", "-y", pkg}
|
||||
if args != nil {
|
||||
baseArgs = append(baseArgs, args...)
|
||||
}
|
||||
return baseCmd, baseArgs
|
||||
}
|
||||
|
||||
// Upgrade returns the command and arguments for upgrading a specific package.
|
||||
func (a *AptManager) Upgrade(pkg, version string) (string, []string) {
|
||||
baseCmd := a.prependAuthCommand(DefaultPackageCommand)
|
||||
baseArgs := []string{"update", "&&", baseCmd, "install", "--only-upgrade", "-y "}
|
||||
if version != "" {
|
||||
baseArgs = append(baseArgs, fmt.Sprintf("%s=%s", pkg, version))
|
||||
} else {
|
||||
baseArgs = append(baseArgs, pkg)
|
||||
}
|
||||
return baseCmd, baseArgs
|
||||
}
|
||||
|
||||
// UpgradeAll returns the command and arguments for upgrading all packages.
|
||||
func (a *AptManager) UpgradeAll() (string, []string) {
|
||||
baseCmd := a.prependAuthCommand(DefaultPackageCommand)
|
||||
baseArgs := []string{"update", "&&", baseCmd, "upgrade", "-y"}
|
||||
return baseCmd, baseArgs
|
||||
}
|
||||
|
||||
// Configure applies functional options to customize the package manager.
|
||||
func (a *AptManager) Configure(options ...pkgcommon.PackageManagerOption) {
|
||||
for _, opt := range options {
|
||||
opt(a)
|
||||
}
|
||||
}
|
||||
|
||||
// prependAuthCommand prepends the authentication command if UseAuth is true.
|
||||
func (a *AptManager) prependAuthCommand(baseCmd string) string {
|
||||
if a.useAuth {
|
||||
return a.authCommand + " " + baseCmd
|
||||
}
|
||||
return baseCmd
|
||||
}
|
||||
|
||||
// SetUseAuth enables or disables authentication.
|
||||
func (a *AptManager) SetUseAuth(useAuth bool) {
|
||||
a.useAuth = useAuth
|
||||
}
|
||||
|
||||
// SetAuthCommand sets the authentication command.
|
||||
func (a *AptManager) SetAuthCommand(authCommand string) {
|
||||
a.authCommand = authCommand
|
||||
}
|
93
pkg/pkgman/dnf/dnf.go
Normal file
93
pkg/pkgman/dnf/dnf.go
Normal file
@ -0,0 +1,93 @@
|
||||
package dnf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/pkgcommon"
|
||||
)
|
||||
|
||||
// DnfManager implements PackageManager for systems using YUM.
|
||||
type DnfManager struct {
|
||||
useAuth bool // Whether to use an authentication command
|
||||
authCommand string // The authentication command, e.g., "sudo"
|
||||
}
|
||||
|
||||
// DefaultAuthCommand is the default command used for authentication.
|
||||
const DefaultAuthCommand = "sudo"
|
||||
|
||||
// NewDnfManager creates a new DnfManager with default settings.
|
||||
func NewDnfManager() *DnfManager {
|
||||
return &DnfManager{
|
||||
useAuth: true,
|
||||
authCommand: DefaultAuthCommand,
|
||||
}
|
||||
}
|
||||
|
||||
// Configure applies functional options to customize the package manager.
|
||||
func (y *DnfManager) Configure(options ...pkgcommon.PackageManagerOption) {
|
||||
for _, opt := range options {
|
||||
opt(y)
|
||||
}
|
||||
}
|
||||
|
||||
// Install returns the command and arguments for installing a package.
|
||||
func (y *DnfManager) Install(pkg, version string, args []string) (string, []string) {
|
||||
baseCmd := y.prependAuthCommand("dnf")
|
||||
baseArgs := []string{"install", "-y"}
|
||||
if version != "" {
|
||||
baseArgs = append(baseArgs, fmt.Sprintf("%s-%s", pkg, version))
|
||||
} else {
|
||||
baseArgs = append(baseArgs, pkg)
|
||||
}
|
||||
if args != nil {
|
||||
baseArgs = append(baseArgs, args...)
|
||||
}
|
||||
return baseCmd, baseArgs
|
||||
}
|
||||
|
||||
// Remove returns the command and arguments for removing a package.
|
||||
func (y *DnfManager) Remove(pkg string, args []string) (string, []string) {
|
||||
baseCmd := y.prependAuthCommand("dnf")
|
||||
baseArgs := []string{"remove", "-y", pkg}
|
||||
if args != nil {
|
||||
baseArgs = append(baseArgs, args...)
|
||||
}
|
||||
return baseCmd, baseArgs
|
||||
}
|
||||
|
||||
// Upgrade returns the command and arguments for upgrading a specific package.
|
||||
func (y *DnfManager) Upgrade(pkg, version string) (string, []string) {
|
||||
baseCmd := y.prependAuthCommand("dnf")
|
||||
baseArgs := []string{"update", "-y"}
|
||||
if version != "" {
|
||||
baseArgs = append(baseArgs, fmt.Sprintf("%s-%s", pkg, version))
|
||||
} else {
|
||||
baseArgs = append(baseArgs, pkg)
|
||||
}
|
||||
return baseCmd, baseArgs
|
||||
}
|
||||
|
||||
// UpgradeAll returns the command and arguments for upgrading all packages.
|
||||
func (y *DnfManager) UpgradeAll() (string, []string) {
|
||||
baseCmd := y.prependAuthCommand("dnf")
|
||||
baseArgs := []string{"update", "-y"}
|
||||
return baseCmd, baseArgs
|
||||
}
|
||||
|
||||
// prependAuthCommand prepends the authentication command if UseAuth is true.
|
||||
func (y *DnfManager) prependAuthCommand(baseCmd string) string {
|
||||
if y.useAuth {
|
||||
return y.authCommand + " " + baseCmd
|
||||
}
|
||||
return baseCmd
|
||||
}
|
||||
|
||||
// SetUseAuth enables or disables authentication.
|
||||
func (y *DnfManager) SetUseAuth(useAuth bool) {
|
||||
y.useAuth = useAuth
|
||||
}
|
||||
|
||||
// SetAuthCommand sets the authentication command.
|
||||
func (y *DnfManager) SetAuthCommand(authCommand string) {
|
||||
y.authCommand = authCommand
|
||||
}
|
4
pkg/pkgman/pkgcommon/options.go
Normal file
4
pkg/pkgman/pkgcommon/options.go
Normal file
@ -0,0 +1,4 @@
|
||||
package pkgcommon
|
||||
|
||||
// PackageManagerOption defines a functional option for configuring a PackageManager.
|
||||
type PackageManagerOption func(interface{})
|
72
pkg/pkgman/pkgman.go
Normal file
72
pkg/pkgman/pkgman.go
Normal file
@ -0,0 +1,72 @@
|
||||
package pkgman
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/apt"
|
||||
"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/dnf"
|
||||
"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/pkgcommon"
|
||||
"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/yum"
|
||||
)
|
||||
|
||||
// PackageManager is an interface used to define common package commands. This shall be implemented by every package.
|
||||
type PackageManager interface {
|
||||
Install(pkg, version string, args []string) (string, []string)
|
||||
Remove(pkg string, args []string) (string, []string)
|
||||
Upgrade(pkg, version string) (string, []string) // Upgrade a specific package
|
||||
UpgradeAll() (string, []string)
|
||||
|
||||
// Configure applies functional options to customize the package manager.
|
||||
Configure(options ...pkgcommon.PackageManagerOption)
|
||||
}
|
||||
|
||||
// PackageManagerFactory returns the appropriate PackageManager based on the package tool.
|
||||
// Takes variable number of options.
|
||||
func PackageManagerFactory(managerType string, options ...pkgcommon.PackageManagerOption) (PackageManager, error) {
|
||||
var manager PackageManager
|
||||
|
||||
switch managerType {
|
||||
case "apt":
|
||||
manager = apt.NewAptManager()
|
||||
case "yum":
|
||||
manager = yum.NewYumManager()
|
||||
case "dnf":
|
||||
manager = dnf.NewDnfManager()
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported package manager: %s", managerType)
|
||||
}
|
||||
|
||||
// Apply options to the manager
|
||||
manager.Configure(options...)
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
// WithAuth enables authentication and sets the authentication command.
|
||||
func WithAuth(authCommand string) pkgcommon.PackageManagerOption {
|
||||
return func(manager interface{}) {
|
||||
if configurable, ok := manager.(interface {
|
||||
SetUseAuth(bool)
|
||||
SetAuthCommand(string)
|
||||
}); ok {
|
||||
configurable.SetUseAuth(true)
|
||||
configurable.SetAuthCommand(authCommand)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithoutAuth disables authentication.
|
||||
func WithoutAuth() pkgcommon.PackageManagerOption {
|
||||
return func(manager interface{}) {
|
||||
if configurable, ok := manager.(interface {
|
||||
SetUseAuth(bool)
|
||||
}); ok {
|
||||
configurable.SetUseAuth(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigurablePackageManager defines methods for setting configuration options.
|
||||
type ConfigurablePackageManager interface {
|
||||
SetUseAuth(useAuth bool)
|
||||
SetAuthCommand(authCommand string)
|
||||
}
|
93
pkg/pkgman/yum/yum.go
Normal file
93
pkg/pkgman/yum/yum.go
Normal file
@ -0,0 +1,93 @@
|
||||
package yum
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/pkgcommon"
|
||||
)
|
||||
|
||||
// YumManager implements PackageManager for systems using YUM.
|
||||
type YumManager struct {
|
||||
useAuth bool // Whether to use an authentication command
|
||||
authCommand string // The authentication command, e.g., "sudo"
|
||||
}
|
||||
|
||||
// DefaultAuthCommand is the default command used for authentication.
|
||||
const DefaultAuthCommand = "sudo"
|
||||
|
||||
// NewYumManager creates a new YumManager with default settings.
|
||||
func NewYumManager() *YumManager {
|
||||
return &YumManager{
|
||||
useAuth: true,
|
||||
authCommand: DefaultAuthCommand,
|
||||
}
|
||||
}
|
||||
|
||||
// Configure applies functional options to customize the package manager.
|
||||
func (y *YumManager) Configure(options ...pkgcommon.PackageManagerOption) {
|
||||
for _, opt := range options {
|
||||
opt(y)
|
||||
}
|
||||
}
|
||||
|
||||
// Install returns the command and arguments for installing a package.
|
||||
func (y *YumManager) Install(pkg, version string, args []string) (string, []string) {
|
||||
baseCmd := y.prependAuthCommand("yum")
|
||||
baseArgs := []string{"install", "-y"}
|
||||
if version != "" {
|
||||
baseArgs = append(baseArgs, fmt.Sprintf("%s-%s", pkg, version))
|
||||
} else {
|
||||
baseArgs = append(baseArgs, pkg)
|
||||
}
|
||||
if args != nil {
|
||||
baseArgs = append(baseArgs, args...)
|
||||
}
|
||||
return baseCmd, baseArgs
|
||||
}
|
||||
|
||||
// Remove returns the command and arguments for removing a package.
|
||||
func (y *YumManager) Remove(pkg string, args []string) (string, []string) {
|
||||
baseCmd := y.prependAuthCommand("yum")
|
||||
baseArgs := []string{"remove", "-y", pkg}
|
||||
if args != nil {
|
||||
baseArgs = append(baseArgs, args...)
|
||||
}
|
||||
return baseCmd, baseArgs
|
||||
}
|
||||
|
||||
// Upgrade returns the command and arguments for upgrading a specific package.
|
||||
func (y *YumManager) Upgrade(pkg, version string) (string, []string) {
|
||||
baseCmd := y.prependAuthCommand("yum")
|
||||
baseArgs := []string{"update", "-y"}
|
||||
if version != "" {
|
||||
baseArgs = append(baseArgs, fmt.Sprintf("%s-%s", pkg, version))
|
||||
} else {
|
||||
baseArgs = append(baseArgs, pkg)
|
||||
}
|
||||
return baseCmd, baseArgs
|
||||
}
|
||||
|
||||
// UpgradeAll returns the command and arguments for upgrading all packages.
|
||||
func (y *YumManager) UpgradeAll() (string, []string) {
|
||||
baseCmd := y.prependAuthCommand("yum")
|
||||
baseArgs := []string{"update", "-y"}
|
||||
return baseCmd, baseArgs
|
||||
}
|
||||
|
||||
// prependAuthCommand prepends the authentication command if UseAuth is true.
|
||||
func (y *YumManager) prependAuthCommand(baseCmd string) string {
|
||||
if y.useAuth {
|
||||
return y.authCommand + " " + baseCmd
|
||||
}
|
||||
return baseCmd
|
||||
}
|
||||
|
||||
// SetUseAuth enables or disables authentication.
|
||||
func (y *YumManager) SetUseAuth(useAuth bool) {
|
||||
y.useAuth = useAuth
|
||||
}
|
||||
|
||||
// SetAuthCommand sets the authentication command.
|
||||
func (y *YumManager) SetAuthCommand(authCommand string) {
|
||||
y.authCommand = authCommand
|
||||
}
|
Reference in New Issue
Block a user