v0.8.1 WIP

This commit is contained in:
2025-02-23 15:33:17 -06:00
parent 1ad50ebcf8
commit 98d8b8e8f2
14 changed files with 200 additions and 61 deletions

View File

@ -47,22 +47,19 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
outputArr []string // holds the output strings returned by processes
)
// Get the command type
// This must be done before concatenating the arguments
command = getCommandType(command)
// Getting the command type must be done before concatenating the arguments
command = getCommandTypeAndSetCommandInfo(command)
for _, v := range command.Args {
ArgsStr += fmt.Sprintf(" %s", v)
}
// print the user's password if it is updated
if command.Type == "user" {
if command.Type == User {
if command.UserOperation == "password" {
cmdCtxLogger.Info().Str("password", command.UserPassword).Msg("user password to be updated")
}
}
// is host defined
if command.Host != nil {
outputArr, errSSH = command.RunCmdSSH(cmdCtxLogger, opts)
if errSSH != nil {
@ -71,7 +68,7 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
} else {
// Handle package operations
if command.Type == "package" && command.PackageOperation == "checkVersion" {
if command.Type == Package && command.PackageOperation == "checkVersion" {
cmdCtxLogger.Info().Str("package", command.PackageName).Msg("Checking package versions")
// Execute the package version command
@ -88,6 +85,64 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
}
var localCMD *exec.Cmd
if command.Type == RemoteScript {
script, err := command.Fetcher.Fetch(command.Cmd)
if err != nil {
return nil, err
}
if command.Shell == "" {
command.Shell = "sh"
}
localCMD = exec.Command(command.Shell, command.Args...)
injectEnvIntoLocalCMD(envVars, localCMD, cmdCtxLogger)
cmdOutWriters = io.MultiWriter(&cmdOutBuf)
if IsCmdStdOutEnabled() {
cmdOutWriters = io.MultiWriter(os.Stdout, &cmdOutBuf)
}
if command.OutputFile != "" {
file, err := os.Create(command.OutputFile)
if err != nil {
return nil, fmt.Errorf("error creating output file: %w", err)
}
defer file.Close()
cmdOutWriters = io.MultiWriter(file, &cmdOutBuf)
if IsCmdStdOutEnabled() {
cmdOutWriters = io.MultiWriter(os.Stdout, file, &cmdOutBuf)
}
}
localCMD.Stdin = bytes.NewReader(script)
localCMD.Stdout = cmdOutWriters
localCMD.Stderr = cmdOutWriters
cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running remoteScript %s on local machine in %s", command.Cmd, command.Shell)).Send()
err = localCMD.Run()
if err != nil {
return nil, fmt.Errorf("error running remote script: %w", err)
}
outScanner := bufio.NewScanner(&cmdOutBuf)
for outScanner.Scan() {
outMap := make(map[string]interface{})
outMap["cmd"] = command.Cmd
outMap["output"] = outScanner.Text()
if str, ok := outMap["output"].(string); ok {
outputArr = append(outputArr, str)
}
if command.OutputToLog {
cmdCtxLogger.Info().Fields(outMap).Send()
}
}
return outputArr, nil
}
var err error
if command.Shell != "" {
cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s on local machine in %s", command.Name, command.Shell)).Send()
@ -101,7 +156,7 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s on local machine", command.Name)).Send()
// execute package commands in a shell
if command.Type == "package" {
if command.Type == Package {
cmdCtxLogger.Info().Str("package", command.PackageName).Msg("Executing package command")
ArgsStr = fmt.Sprintf("%s %s", command.Cmd, ArgsStr)
localCMD = exec.Command("/bin/sh", "-c", ArgsStr)

View File

@ -578,7 +578,7 @@ func processCmds(opts *ConfigOpts) error {
}
// Parse package commands
if cmd.Type == "package" {
if cmd.Type == Package {
if cmd.PackageManager == "" {
return fmt.Errorf("package manager is required for package command %s", cmd.PackageName)
}
@ -603,7 +603,7 @@ func processCmds(opts *ConfigOpts) error {
}
// Parse user commands
if cmd.Type == "user" {
if cmd.Type == User {
if cmd.Username == "" {
return fmt.Errorf("username is required for user command %s", cmd.Name)
}
@ -630,12 +630,24 @@ func processCmds(opts *ConfigOpts) error {
}
if cmd.Type == "remoteScript" {
if cmd.Type == RemoteScript {
var fetchErr error
if !isRemoteURL(cmd.Cmd) {
return fmt.Errorf("remoteScript command %s must be a remote resource", cmdName)
}
cmd.Fetcher, fetchErr = remotefetcher.NewRemoteFetcher(cmd.Cmd, opts.Cache, remotefetcher.WithFileType("script"))
if fetchErr != nil {
return fmt.Errorf("error initializing remote fetcher for remoteScript: %v", fetchErr)
}
}
if cmd.OutputFile != "" {
var err error
cmd.OutputFile, err = resolveDir(cmd.OutputFile)
if err != nil {
return err
}
}
}
return nil
}

View File

@ -64,6 +64,11 @@ func (opts *ConfigOpts) ListCommand(cmd string) {
println()
}
if cmdInfo.Type.String() != "" {
print("Type: ", cmdInfo.Type.String())
println()
}
} else {
fmt.Printf("Command %s not found. Check spelling.\n", cmd)

View File

@ -252,7 +252,6 @@ func (remoteHost *Host) GetPort() {
// 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 == "" {
@ -315,7 +314,6 @@ func (remoteHost *Host) ConnectThroughBastion(log zerolog.Logger) (*ssh.Client,
return nil, err
}
// sClient is an ssh client connected to the service host, through the bastion host.
sClient := ssh.NewClient(ncc, chans, reqs)
return sClient, nil
@ -480,7 +478,6 @@ 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
@ -492,10 +489,8 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts)
env: command.Environment,
}
)
// Get the command type
// This must be done before concatenating the arguments
command.Type = strings.TrimSpace(command.Type)
command = getCommandType(command)
// Getting the command type must be done before concatenating the arguments
command = getCommandTypeAndSetCommandInfo(command)
// Prepare command arguments
for _, v := range command.Args {
@ -505,7 +500,7 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts)
cmdCtxLogger.Info().
Str("Command", command.Name).
Str("Host", *command.Host).
Msgf("Running %s on host %s", getCommandTypeLabel(command.Type), *command.Host)
Msgf("Running %s on host %s", getCommandTypeAndSetCommandInfoLabel(command.Type), *command.Host)
// cmdCtxLogger.Debug().Str("cmd", command.Cmd).Strs("args", command.Args).Send()
@ -536,11 +531,13 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts)
// Handle command execution based on type
switch command.Type {
case "script":
case Script:
return command.runScript(commandSession, cmdCtxLogger, &cmdOutBuf)
case "scriptFile":
case RemoteScript:
return command.runRemoteScript(commandSession, cmdCtxLogger, &cmdOutBuf)
case ScriptFile:
return command.runScriptFile(commandSession, cmdCtxLogger, &cmdOutBuf)
case "package":
case Package:
if command.PackageOperation == "checkVersion" {
commandSession.Stderr = nil
// Execute the package version command remotely
@ -558,7 +555,7 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts)
cmdCtxLogger.Debug().Str("cmd + args", ArgsStr).Send()
// Run simple command
if err := commandSession.Run(ArgsStr); err != nil {
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, true), fmt.Errorf("error running command: %w", err)
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), fmt.Errorf("error running command: %w", err)
}
}
default:
@ -570,11 +567,11 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts)
cmdCtxLogger.Debug().Str("cmd + args", ArgsStr).Send()
// Run simple command
if err := commandSession.Run(ArgsStr); err != nil {
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.GetOutput), fmt.Errorf("error running command: %w", err)
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), fmt.Errorf("error running command: %w", err)
}
}
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, true), nil
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), nil
}
func checkPackageVersion(cmdCtxLogger zerolog.Logger, command *Command, commandSession *ssh.Session, cmdOutBuf bytes.Buffer) ([]string, error) {
@ -593,17 +590,17 @@ func checkPackageVersion(cmdCtxLogger zerolog.Logger, command *Command, commandS
_, parseErr := parsePackageVersion(string(cmdOut), cmdCtxLogger, command, cmdOutBuf)
if parseErr != nil {
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.GetOutput), fmt.Errorf("error: package %s not listed: %w", command.PackageName, err)
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), fmt.Errorf("error: package %s not listed: %w", command.PackageName, err)
}
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.GetOutput), fmt.Errorf("error running %s: %w", ArgsStr, err)
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), fmt.Errorf("error running %s: %w", ArgsStr, err)
}
return parsePackageVersion(string(cmdOut), cmdCtxLogger, command, cmdOutBuf)
}
// getCommandTypeLabel returns a human-readable label for the command type.
func getCommandTypeLabel(commandType string) string {
if commandType == "" {
// getCommandTypeAndSetCommandInfoLabel returns a human-readable label for the command type.
func getCommandTypeAndSetCommandInfoLabel(commandType CommandType) string {
if !commandType.IsACommandType() {
return "command"
}
return fmt.Sprintf("%s command", commandType)
@ -644,7 +641,7 @@ func (command *Command) runScriptFile(session *ssh.Session, cmdCtxLogger zerolog
return collectOutput(outputBuf, command.Name, cmdCtxLogger, true), fmt.Errorf("error waiting for shell: %w", err)
}
return collectOutput(outputBuf, command.Name, cmdCtxLogger, true), nil
return collectOutput(outputBuf, command.Name, cmdCtxLogger, command.OutputToLog), nil
}
// prepareScriptBuffer prepares a buffer for inline scripts.
@ -688,6 +685,25 @@ func (command *Command) prepareScriptFileBuffer() (*bytes.Buffer, error) {
return &buffer, nil
}
// runRemoteScript handles the execution of remote scripts
func (command *Command) runRemoteScript(session *ssh.Session, cmdCtxLogger zerolog.Logger, outputBuf *bytes.Buffer) ([]string, error) {
script, err := command.Fetcher.Fetch(command.Cmd)
if err != nil {
return nil, err
}
if command.Shell == "" {
command.Shell = "sh"
}
session.Stdin = bytes.NewReader(script)
err = session.Run(command.Shell)
if err != nil {
return collectOutput(outputBuf, command.Name, cmdCtxLogger, command.OutputToLog), fmt.Errorf("error running remote script: %w", err)
}
return collectOutput(outputBuf, command.Name, cmdCtxLogger, command.OutputToLog), nil
}
// readFileToBuffer reads a file into a buffer.
func readFileToBuffer(filePath string) (*bytes.Buffer, error) {
resolvedPath, err := resolveDir(filePath)

View File

@ -54,9 +54,8 @@ type (
// command to run
Cmd string `yaml:"cmd"`
// Possible values: script, scriptFile
// If blank, it is regular command.
Type string `yaml:"type,omitempty"`
// See CommandType enum further down the page for acceptable values
Type CommandType `yaml:"type,omitempty"`
// host on which to run cmd
Host *string `yaml:"host,omitempty"`
@ -93,6 +92,10 @@ type (
ScriptEnvFile string `yaml:"scriptEnvFile"`
OutputToLog bool `yaml:"outputToLog,omitempty"`
OutputFile string `yaml:"outputFile,omitempty"`
// BEGIN PACKAGE COMMAND FIELDS
PackageManager string `yaml:"packageManager,omitempty"`
@ -116,6 +119,8 @@ type (
// FetchBeforeExecution determines if the remoteSource should be fetched before running
FetchBeforeExecution bool `yaml:"fetchBeforeExecution,omitempty"`
Fetcher remotefetcher.RemoteFetcher
// BEGIN USER COMMAND FIELDS
// Username specifies the username for user creation or related operations
@ -289,4 +294,15 @@ type (
ListName string // Name of the command list
Error error // Error encountered, if any
}
CommandType int
)
//go:generate go run github.com/dmarkham/enumer -linecomment -yaml -text -json -type=CommandType
const (
Default CommandType = iota //
Script // script
ScriptFile // scriptFile
RemoteScript // remoteScript
Package // package
User // user
)

View File

@ -251,12 +251,12 @@ func expandEnvVars(backyEnv map[string]string, envVars []string) {
}
}
// getCommandType checks for command type and if the command has already been set
// getCommandTypeAndSetCommandInfo checks for command type and if the command has already been set
// Checks for types package and user
// Returns the modified Command with the package- or userManager command as Cmd and the package- or userOperation as args, plus any additional Args
func getCommandType(command *Command) *Command {
func getCommandTypeAndSetCommandInfo(command *Command) *Command {
if command.Type == "package" && !command.packageCmdSet {
if command.Type == Package && !command.packageCmdSet {
command.packageCmdSet = true
switch command.PackageOperation {
case "install":
@ -270,7 +270,7 @@ func getCommandType(command *Command) *Command {
}
}
if command.Type == "user" && !command.userCmdSet {
if command.Type == User && !command.userCmdSet {
command.userCmdSet = true
switch command.UserOperation {
case "add":

View File

@ -16,6 +16,7 @@ type CacheData struct {
Hash string `yaml:"hash"`
Path string `yaml:"path"`
Type string `yaml:"type"`
URL string `yaml:"url"`
}
type Cache struct {
@ -101,13 +102,17 @@ func (c *Cache) AddDataToStore(hash string, cacheData CacheData) error {
return c.saveToFile()
}
// Set stores data on disk and in the cache file and returns the cache data
// The filepath of the data is the file name + a SHA256 hash of the URL
func (c *Cache) Set(source, hash string, data []byte, dataType string) (CacheData, error) {
c.mu.Lock()
defer c.mu.Unlock()
sourceHash := HashURL(source)
fileName := filepath.Base(source)
path := filepath.Join(c.dir, fmt.Sprintf("%s-%s", fileName, hash))
path := filepath.Join(c.dir, fmt.Sprintf("%s-%s", fileName, sourceHash))
if _, err := os.Stat(path); os.IsNotExist(err) {
os.MkdirAll(c.dir, 0700)
@ -122,9 +127,10 @@ func (c *Cache) Set(source, hash string, data []byte, dataType string) (CacheDat
Hash: hash,
Path: path,
Type: dataType,
URL: sourceHash,
}
c.store[hash] = cacheData
c.store[sourceHash] = cacheData
// Unlock before calling saveToFile to avoid double-locking
c.mu.Unlock()
@ -184,3 +190,8 @@ func LoadMetadataFromFile(filePath string) ([]*CacheData, error) {
return cacheData, nil
}
func HashURL(url string) string {
hash := sha256.Sum256([]byte(url))
return hex.EncodeToString(hash[:])
}

View File

@ -57,11 +57,12 @@ func NewRemoteFetcher(source string, cache *Cache, options ...FetcherOption) (Re
return nil, err
}
hash := fetcher.Hash(data)
if cachedData, cacheMeta, exists := cache.Get(hash); exists {
URLHash := HashURL(source)
if cachedData, cacheMeta, exists := cache.Get(URLHash); exists {
return &CachedFetcher{data: cachedData, path: cacheMeta.Path, dataType: cacheMeta.Type}, nil
}
hash := fetcher.Hash(data)
cacheData, err := cache.Set(source, hash, data, config.FileType)
if err != nil {
return nil, err