Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/improve error message #35

Merged
merged 11 commits into from
Sep 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ NAME := sake
PACKAGE := github.com/alajmo/$(NAME)
DATE := $(shell date +%FT%T%Z)
GIT := $(shell [ -d .git ] && git rev-parse --short HEAD)
VERSION := v0.10.3
VERSION := v0.11.0

default: build

Expand All @@ -15,6 +15,7 @@ gofmt:
go fmt ./core/dao/***.go
go fmt ./core/run/***.go
go fmt ./core/print/***.go
go fmt ./test/integration/***.go

lint:
golangci-lint run ./cmd/... ./core/... ./test/...
Expand Down
8 changes: 5 additions & 3 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ You define servers and tasks in a sake.yaml config file and then run the tasks o
var (
config dao.Config
configErr error
configFilepath string
configPath string
userConfigPath string
sshConfigPath string
noColor bool
buildMode = ""
version = "dev"
Expand Down Expand Up @@ -55,8 +56,9 @@ func init() {

cobra.OnInitialize(initConfig)

rootCmd.PersistentFlags().StringVarP(&configFilepath, "config", "c", "", "specify config")
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "specify config")
rootCmd.PersistentFlags().StringVarP(&userConfigPath, "user-config", "u", "", "specify user config")
rootCmd.PersistentFlags().StringVarP(&sshConfigPath, "ssh-config", "U", "", "specify ssh config")
rootCmd.PersistentFlags().BoolVar(&noColor, "no-color", false, "disable color")

rootCmd.AddCommand(
Expand All @@ -80,5 +82,5 @@ func init() {
}

func initConfig() {
config, configErr = dao.ReadConfig(configFilepath, userConfigPath, noColor)
config, configErr = dao.ReadConfig(configPath, userConfigPath, sshConfigPath, noColor)
}
5 changes: 4 additions & 1 deletion cmd/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ func ssh(args []string, config *dao.Config) {
core.CheckIfError(err)
servers := []dao.Server{*server}

err = run.ParseServers(&servers)
errConnect, err := run.ParseServers(config.SSHConfigFile, &servers)
if len(errConnect) > 0 {
core.Exit(&errConnect[0])
}
core.CheckIfError(err)

err = run.SSHToServer(*server, config.DisableVerifyHost, config.KnownHostsFile)
Expand Down
4 changes: 4 additions & 0 deletions core/config.man
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,10 @@ Override config file path
.B SAKE_USER_CONFIG
Override user config file path

.TP
.B SAKE_SSH_CONFIG
Override ssh config file path

.TP
.B SAKE_KNOWN_HOSTS_FILE
Override known_hosts file path
Expand Down
41 changes: 40 additions & 1 deletion core/dao/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ var (
)

type Config struct {
SSHConfigFile *string
DisableVerifyHost bool
KnownHostsFile string
Shell string
Expand Down Expand Up @@ -87,11 +88,15 @@ func (c *ConfigYAML) GetContextLine() int {
}

// Function to read sake configs.
func ReadConfig(configFilepath string, userConfigPath string, noColor bool) (Config, error) {
func ReadConfig(configFilepath string, userConfigPath string, sshConfigFile string, noColor bool) (Config, error) {
CheckUserNoColor(noColor)
var configPath string

userConfigFile := getUserConfigFile(userConfigPath)
sshConfigPath, err := getSSHConfigPath(sshConfigFile)
if err != nil {
return Config{}, err
}

// Try to find config file in current directory and all parents
if configFilepath != "" {
Expand Down Expand Up @@ -147,6 +152,7 @@ func ReadConfig(configFilepath string, userConfigPath string, noColor bool) (Con
}

config, configErr := configYAML.parseConfig()
config.SSHConfigFile = sshConfigPath
config.CheckConfigNoColor()

if configErr != nil {
Expand Down Expand Up @@ -180,6 +186,39 @@ func getUserConfigFile(userConfigPath string) *string {
return nil
}

func getSSHConfigPath(sshConfigPath string) (*string, error) {
// Flag
if sshConfigPath != "" {
if _, err := os.Stat(sshConfigPath); err == nil {
return &sshConfigPath, nil
} else {
return &sshConfigPath, err
}
}

// Env
val, present := os.LookupEnv("SAKE_SSH_CONFIG")
if present {
return &val, nil
}

// User SSH config
if home, err := os.UserHomeDir(); err == nil {
userSSHConfigFile := filepath.Join(home, ".ssh", "config")
if _, err := os.Stat(userSSHConfigFile); err == nil {
return &userSSHConfigFile, nil
}
}

// Global SSH config
globalSSHConfig := "/etc/ssh/ssh_config"
if _, err := os.Stat(globalSSHConfig); err == nil {
return &globalSSHConfig, nil
}

return nil, nil
}

// Open sake config in editor
func (c Config) EditConfig() error {
return openEditor(c.Path, -1)
Expand Down
1 change: 1 addition & 0 deletions core/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ func (f *TemplateParseError) Error() string {
return fmt.Sprintf("failed to parse %s", f.Msg)
}

// If there's a misconfiguration somewhere, not associated with server errors
type ExecError struct {
Err error
ExitCode int
Expand Down
133 changes: 100 additions & 33 deletions core/run/exec.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package run

import (
"errors"
"fmt"
"golang.org/x/crypto/ssh"
"os"
Expand All @@ -11,7 +12,6 @@ import (
"sync"

"github.com/jedib0t/go-pretty/v6/text"
"github.com/kevinburke/ssh_config"

"github.com/alajmo/sake/core"
"github.com/alajmo/sake/core/dao"
Expand Down Expand Up @@ -60,11 +60,27 @@ func (run *Run) RunTask(
return err
}

err = ParseServers(&run.Servers)
errConnects, err := ParseServers(run.Config.SSHConfigFile, &run.Servers)
if err != nil {
return err
}

if len(errConnects) > 0 {
parseOutput := dao.TableOutput{
Headers: []string{"server", "host", "user", "port", "error"},
Rows: []dao.Row{},
}

for _, u := range errConnects {
parseOutput.Rows = append(parseOutput.Rows, dao.Row{Columns: []string{u.Name, u.Host, u.User, strconv.Itoa(int(u.Port)), u.Reason}})
}

options := print.PrintTableOptions{Theme: task.Theme, OmitEmpty: task.Spec.OmitEmpty, Output: task.Spec.Output, SuppressEmptyColumns: false}
print.PrintTable("Parse Errors", parseOutput.Rows, options, parseOutput.Headers[0:1], parseOutput.Headers[1:])

return &core.ExecError{Err: errors.New("Parse Error"), ExitCode: 4}
}

err = run.ParseTask(configEnv, userArgs, runFlags, setRunFlags)
if err != nil {
return err
Expand Down Expand Up @@ -355,85 +371,136 @@ func (run *Run) CleanupClients() {
}

// ParseServers resolves host, port, proxyjump in users ssh config
func ParseServers(servers *[]dao.Server) error {
func ParseServers(sshConfigFile *string, servers *[]dao.Server) ([]ErrConnect, error) {
if sshConfigFile == nil {
return nil, nil
}

cfg, err := core.ParseFile(*sshConfigFile)
if err != nil {
return nil, err
}

var errConnects []ErrConnect
for i := range *servers {
serv := cfg[(*servers)[i].Host]

// Bastion resolve:
// 1. proxyjump alias
// 2. proxyjump
// 3. bastion alias
if proxyJump := ssh_config.Get((*servers)[i].Host, "ProxyJump"); proxyJump != "" {
if hostName := ssh_config.Get(proxyJump, "HostName"); hostName != "" {
if proxyJump := serv.ProxyJump; proxyJump != "" {
if hostName := cfg[proxyJump].HostName; hostName != "" {
// 1. proxyjump alias
user := ssh_config.Get(proxyJump, "User")
if user != "" {
(*servers)[i].BastionUser = user
} else {
(*servers)[i].BastionUser = (*servers)[i].User
}
(*servers)[i].BastionHost = hostName

port := ssh_config.Get(proxyJump, "Port")
port := cfg[proxyJump].Port
if port != "" {
p, err := strconv.ParseInt(port, 10, 16)
if err != nil {
return err
errConnect := &ErrConnect{
Name: (*servers)[i].Name,
User: (*servers)[i].User,
Host: (*servers)[i].Host,
Port: (*servers)[i].Port,
Reason: err.Error(),
}
errConnects = append(errConnects, *errConnect)
continue
}
(*servers)[i].BastionPort = uint16(p)
} else {
(*servers)[i].BastionPort = (*servers)[i].Port
}

(*servers)[i].BastionHost = hostName
user := cfg[proxyJump].User
if user != "" {
(*servers)[i].BastionUser = user
} else {
(*servers)[i].BastionUser = (*servers)[i].User
}
} else {
// 2. proxyjump
user, host, port, err := core.ParseHostName(proxyJump, (*servers)[i].User, (*servers)[i].Port)
if err != nil {
return err
errConnect := &ErrConnect{
Name: (*servers)[i].Name,
User: (*servers)[i].User,
Host: (*servers)[i].Host,
Port: (*servers)[i].Port,
Reason: err.Error(),
}
errConnects = append(errConnects, *errConnect)
continue
}

(*servers)[i].BastionUser = user
(*servers)[i].BastionPort = port
(*servers)[i].BastionHost = host
}
} else if bastionHost := ssh_config.Get((*servers)[i].BastionHost, "HostName"); bastionHost != "" {
// 3. bastion alias

user := ssh_config.Get((*servers)[i].BastionHost, "User")
if user != "" {
(*servers)[i].BastionPort = port
(*servers)[i].BastionUser = user
} else {
(*servers)[i].BastionUser = (*servers)[i].User
}
} else if bastionHost := cfg[(*servers)[i].BastionHost].HostName; bastionHost != "" {
// 3. bastion alias
(*servers)[i].BastionHost = bastionHost

port := ssh_config.Get((*servers)[i].BastionHost, "Port")
port := cfg[(*servers)[i].BastionHost].Port
if port != "" {
p, err := strconv.ParseInt(port, 10, 16)
if err != nil {
return err
errConnect := &ErrConnect{
Name: (*servers)[i].Name,
User: (*servers)[i].User,
Host: (*servers)[i].Host,
Port: (*servers)[i].Port,
Reason: err.Error(),
}
errConnects = append(errConnects, *errConnect)
continue
}
(*servers)[i].BastionPort = uint16(p)
} else {
(*servers)[i].BastionPort = (*servers)[i].Port
}

(*servers)[i].BastionHost = bastionHost
user := cfg[(*servers)[i].BastionHost].User
if user != "" {
(*servers)[i].BastionUser = user
} else {
(*servers)[i].BastionUser = (*servers)[i].User
}
}

host := ssh_config.Get((*servers)[i].Host, "HostName")
// HostName
host := serv.HostName
if host != "" {
(*servers)[i].Host = host
}

port := ssh_config.Get((*servers)[i].Host, "Port")
if port != "22" {
// User
user := serv.User
if user != "" {
(*servers)[i].User = user
}

// Port
port := serv.Port
if port != "" {
p, err := strconv.ParseInt(port, 10, 16)
if err != nil {
return err
errConnect := &ErrConnect{
Name: (*servers)[i].Name,
User: (*servers)[i].User,
Host: (*servers)[i].Host,
Port: (*servers)[i].Port,
Reason: err.Error(),
}
errConnects = append(errConnects, *errConnect)
continue
}
(*servers)[i].Port = uint16(p)
}
}

return nil
return errConnects, err
}

func (run *Run) ParseTask(configEnv []string, userArgs []string, runFlags *core.RunFlags, setRunFlags *core.SetRunFlags) error {
Expand Down
9 changes: 8 additions & 1 deletion core/sake.1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.TH "SAKE" "1" "2022-08-28T18:38:01CEST" "v0.10.3" "Sake Manual" "sake"
.TH "SAKE" "1" "2022-09-16T20:23:01CEST" "v0.11.0" "Sake Manual" "sake"
.SH NAME
sake - sake is a command runner for local and remote hosts

Expand All @@ -22,6 +22,9 @@ help for sake
\fB--no-color[=false]\fR
disable color
.TP
\fB-U, --ssh-config=""\fR
specify ssh config
.TP
\fB-u, --user-config=""\fR
specify user config
.SH
Expand Down Expand Up @@ -824,6 +827,10 @@ Override config file path
.B SAKE_USER_CONFIG
Override user config file path

.TP
.B SAKE_SSH_CONFIG
Override ssh config file path

.TP
.B SAKE_KNOWN_HOSTS_FILE
Override known_hosts file path
Expand Down
Loading