Skip to content
Open
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
bin/
*.sw?
tmp
lab
dist
example
49 changes: 19 additions & 30 deletions cmd/sup/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import (
"fmt"
"io/ioutil"
"os"
"os/user"
"path/filepath"
"regexp"
"strings"
"text/tabwriter"
Expand Down Expand Up @@ -127,14 +125,14 @@ func parseArgs(conf *sup.Supfile) (*sup.Network, []*sup.Command, error) {
if len(env) == 0 {
continue
}
i := strings.Index(env, "=")
if i < 0 {
before, after, ok0 := strings.Cut(env, "=")
if !ok0 {
if len(env) > 0 {
network.Env.Set(env, "")
}
continue
}
network.Env.Set(env[:i], env[i+1:])
network.Env.Set(before, after)
}

hosts, err := network.ParseInventory()
Expand Down Expand Up @@ -208,19 +206,6 @@ func parseArgs(conf *sup.Supfile) (*sup.Network, []*sup.Command, error) {
return &network, commands, nil
}

func resolvePath(path string) string {
if path == "" {
return ""
}
if path[:2] == "~/" {
usr, err := user.Current()
if err == nil {
path = filepath.Join(usr.HomeDir, path[2:])
}
}
return path
}

func main() {
flag.Parse()

Expand All @@ -238,7 +223,7 @@ func main() {
if supfile == "" {
supfile = "./Supfile"
}
data, err := ioutil.ReadFile(resolvePath(supfile))
data, err := os.ReadFile(sup.ResolvePath(supfile))
if err != nil {
firstErr := err
data, err = ioutil.ReadFile("./Supfile.yml") // Alternative to ./Supfile.
Expand Down Expand Up @@ -305,7 +290,7 @@ func main() {

// --sshconfig flag location for ssh_config file
if sshConfig != "" {
confHosts, err := sshconfig.ParseSSHConfig(resolvePath(sshConfig))
confHosts, err := sshconfig.Parse(sup.ResolvePath(sshConfig))
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
Expand All @@ -320,15 +305,19 @@ func main() {
}
}

// check network.Hosts for match
// check network.Hosts for match and expand them
var newHosts []string
for _, host := range network.Hosts {
conf, found := confMap[host]
if found {
newHosts = append(newHosts, fmt.Sprintf("%s:%d", conf.HostName, conf.Port))
network.User = conf.User
network.IdentityFile = resolvePath(conf.IdentityFile)
network.Hosts = []string{fmt.Sprintf("%s:%d", conf.HostName, conf.Port)}
network.IdentityFile = sup.ResolvePath(conf.IdentityFile)
} else {
newHosts = append(newHosts, host)
}
}
network.Hosts = newHosts
}

var vars sup.EnvList
Expand All @@ -346,24 +335,24 @@ func main() {
if len(env) == 0 {
continue
}
i := strings.Index(env, "=")
if i < 0 {
before, after, ok := strings.Cut(env, "=")
if !ok {
if len(env) > 0 {
vars.Set(env, "")
}
continue
}
vars.Set(env[:i], env[i+1:])
cliVars.Set(env[:i], env[i+1:])
vars.Set(before, after)
cliVars.Set(before, after)
}

// SUP_ENV is generated only from CLI env vars.
// Separate loop to omit duplicates.
supEnv := ""
var supEnv strings.Builder
for _, v := range cliVars {
supEnv += fmt.Sprintf(" -e %v=%q", v.Key, v.Value)
supEnv.WriteString(fmt.Sprintf(" -e %v=%q", v.Key, v.Value))
}
vars.Set("SUP_ENV", strings.TrimSpace(supEnv))
vars.Set("SUP_ENV", strings.TrimSpace(supEnv.String()))

// Create new Stackup app.
app, err := sup.New(conf)
Expand Down
19 changes: 19 additions & 0 deletions fn.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package sup

import (
"os/user"
"path/filepath"
)

func ResolvePath(path string) string {
if path == "" {
return ""
}
if path[:2] == "~/" {
usr, err := user.Current()
if err == nil {
path = filepath.Join(usr.HomeDir, path[2:])
}
}
return path
}
24 changes: 18 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
module github.com/pressly/sup

go 1.13
go 1.24.0

require (
github.com/cheggaaa/pb/v3 v3.1.7
github.com/goware/prefixer v0.0.0-20160118172347-395022866408
github.com/kr/pretty v0.2.0 // indirect
github.com/mikkeloscar/sshconfig v0.0.0-20190102082740-ec0822bcc4f4
github.com/mikkeloscar/sshconfig v0.1.1
github.com/pkg/errors v0.9.1
golang.org/x/crypto v0.0.0-20200208060501-ecb85df21340
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 // indirect
golang.org/x/crypto v0.48.0
gopkg.in/yaml.v2 v2.4.0
)

require (
github.com/VividCortex/ewma v1.2.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/kr/pretty v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.20 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/term v0.40.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v2 v2.2.8
)
49 changes: 27 additions & 22 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,34 +1,39 @@
github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow=
github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4=
github.com/cheggaaa/pb/v3 v3.1.7 h1:2FsIW307kt7A/rz/ZI2lvPO+v3wKazzE4K/0LtTWsOI=
github.com/cheggaaa/pb/v3 v3.1.7/go.mod h1:/Ji89zfVPeC/u5j8ukD0MBPHt2bzTYp74lQ7KlgFWTQ=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/goware/prefixer v0.0.0-20160118172347-395022866408 h1:Y9iQJfEqnN3/Nce9cOegemcy/9Ai5k3huT6E80F3zaw=
github.com/goware/prefixer v0.0.0-20160118172347-395022866408/go.mod h1:PE1ycukgRPJ7bJ9a1fdfQ9j8i/cEcRAoLZzbxYpNB/s=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mikkeloscar/sshconfig v0.0.0-20161223095632-fc5e37b16b68 h1:Z1BVWGqEm0aveMz9ffiFnJthFjM5+YFdFqFklQ/hPBI=
github.com/mikkeloscar/sshconfig v0.0.0-20161223095632-fc5e37b16b68/go.mod h1:GvQCIGDpivPr+e8cuBt3c4+NTOJm66zpBrMjkit8jmw=
github.com/mikkeloscar/sshconfig v0.0.0-20190102082740-ec0822bcc4f4 h1:6mjPKnEtYKqYTqIXAraugfl5bkaW+A6wJAupYKAWMXM=
github.com/mikkeloscar/sshconfig v0.0.0-20190102082740-ec0822bcc4f4/go.mod h1:GvQCIGDpivPr+e8cuBt3c4+NTOJm66zpBrMjkit8jmw=
github.com/pkg/errors v0.7.1-0.20160627222352-a2d6902c6d2a h1:dKpZ0nc8i7prliB4AIfJulQxsX7whlVwi6j5HqaYUl4=
github.com/pkg/errors v0.7.1-0.20160627222352-a2d6902c6d2a/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mikkeloscar/sshconfig v0.1.1 h1:WJLz/y4M0jMkYHDJkydcbOb/S8UAJ1denM9fCpwKV5c=
github.com/mikkeloscar/sshconfig v0.1.1/go.mod h1:NavXZq+n9+iOgFT6fOobpl6nFBltLYOIjejTwNQTK7A=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
golang.org/x/crypto v0.0.0-20160804082612-7a1054f3ac58 h1:ytej7jB0ejb21kF+TjEWykw7n4sG85mxyjgYHgF/7ZQ=
golang.org/x/crypto v0.0.0-20160804082612-7a1054f3ac58/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200208060501-ecb85df21340 h1:KOcEaR10tFr7gdJV2GCKw8Os5yED1u1aOqHjOAb6d2Y=
golang.org/x/crypto v0.0.0-20200208060501-ecb85df21340/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.0.0-20160301204022-a83829b6f129 h1:RBgb9aPUbZ9nu66ecQNIBNsA7j3mB5h8PNDIfhPjaJg=
gopkg.in/yaml.v2 v2.0.0-20160301204022-a83829b6f129/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
71 changes: 60 additions & 11 deletions ssh.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package sup

import (
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"os"
"os/user"
Expand All @@ -13,6 +13,7 @@ import (

"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"golang.org/x/crypto/ssh/terminal"
)

// Client is a wrapper over the SSH connection/sessions.
Expand All @@ -29,6 +30,10 @@ type SSHClient struct {
running bool
env string //export FOO="bar"; export BAR="baz";
color string

ask bool // For interactive "ask:root@..."
password string // For config "password: ..."
identityFile string
}

type ErrConnect struct {
Expand All @@ -54,6 +59,15 @@ func (c *SSHClient) parseHost(host string) error {
if at := strings.LastIndex(c.host, "@"); at != -1 {
c.user = c.host[:at]
c.host = c.host[at+1:]

// Check if the username starts with "ask:"
c.ask = false
if after, ok := strings.CutPrefix(c.user, "ask:"); ok {
// Remove "ask:" from the username
c.user = after
// Set the flag so ConnectWith knows to prompt
c.ask = true
}
}

// Add default user, if not set
Expand Down Expand Up @@ -97,7 +111,7 @@ func initAuthMethod() {
if strings.HasSuffix(file, ".pub") {
continue // Skip public keys.
}
data, err := ioutil.ReadFile(file)
data, err := os.ReadFile(file)
if err != nil {
continue
}
Expand Down Expand Up @@ -130,19 +144,54 @@ func (c *SSHClient) ConnectWith(host string, dialer SSHDialFunc) error {

initAuthMethodOnce.Do(initAuthMethod)

err := c.parseHost(host)
if err != nil {
if err := c.parseHost(host); err != nil {
return err
}

auths := []ssh.AuthMethod{
authMethod,
}

if c.identityFile != "" {
resolvedPath := ResolvePath(c.identityFile)
key, err := os.ReadFile(resolvedPath)
if err != nil {
return ErrConnect{c.user, c.host, fmt.Sprintf("reading private key %s: %v", c.identityFile, err)}
}

signer, err := ssh.ParsePrivateKey(key)
if err != nil {
var passphraseMissingError *ssh.PassphraseMissingError
if errors.As(err, &passphraseMissingError) {
return ErrConnect{c.user, c.host, fmt.Sprintf("private key %s is encrypted with passphrase (not supported)", c.identityFile)}
}
return ErrConnect{c.user, c.host, fmt.Sprintf("parsing private key %s: %v", c.identityFile, err)}
}

auths = append([]ssh.AuthMethod{ssh.PublicKeys(signer)}, auths...)
}

if c.password != "" {
auths = append(auths, ssh.Password(c.password))
}

if c.ask {
fmt.Printf("Enter Password for %s@%s: ", c.user, c.host)
pass, err := terminal.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
return err
}
fmt.Println()
auths = append(auths, ssh.Password(string(pass)))
}

config := &ssh.ClientConfig{
User: c.user,
Auth: []ssh.AuthMethod{
authMethod,
},
User: c.user,
Auth: auths,
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}

var err error
c.conn, err = dialer("tcp", c.host, config)
if err != nil {
return ErrConnect{c.user, c.host, err.Error()}
Expand All @@ -155,10 +204,10 @@ func (c *SSHClient) ConnectWith(host string, dialer SSHDialFunc) error {
// Run runs the task.Run command remotely on c.host.
func (c *SSHClient) Run(task *Task) error {
if c.running {
return fmt.Errorf("Session already running")
return fmt.Errorf("session already running")
}
if c.sessOpened {
return fmt.Errorf("Session already connected")
return fmt.Errorf("session already connected")
}

sess, err := c.conn.NewSession()
Expand Down Expand Up @@ -241,7 +290,7 @@ func (c *SSHClient) Close() error {
c.sessOpened = false
}
if !c.connOpened {
return fmt.Errorf("Trying to close the already closed connection")
return fmt.Errorf("trying to close the already closed connection")
}

err := c.conn.Close()
Expand Down
Loading