This commit is contained in:
2025-01-03 23:30:07 -06:00
parent 01efeab13f
commit b5f7c3fd72
818 changed files with 1112 additions and 82997 deletions

View File

@ -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()
}
}
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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
View 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
View 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
}

View 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
View 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
View 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
}