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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Stack Up is a simple deployment tool that performs given set of commands on mult
| Option | Description |
|-------------------|----------------------------------|
| `-f Supfile` | Custom path to Supfile |
| `-i`, `sshKey` | Set the the ssh key to use |
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They call it identity_file in ssh command. I'm thinking if we should be consistent with them.

| `-e`, `--env=[]` | Set environment variables |
| `--only REGEXP` | Filter hosts matching regexp |
| `--except REGEXP` | Filter out hosts matching regexp |
Expand All @@ -43,6 +44,8 @@ networks:
- api1.example.com
- api2.example.com
- api3.example.com
# Optional, override the ssh key to use for this network
ssh-key: ~/.ssh/prodKey
staging:
# fetch dynamic list of hosts
inventory: curl http://example.com/latest/meta-data/hostname
Expand Down
3 changes: 3 additions & 0 deletions cmd/sup/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
var (
supfile string
envVars flagStringSlice
sshKey string
onlyHosts string
exceptHosts string

Expand Down Expand Up @@ -46,6 +47,7 @@ func (f *flagStringSlice) Set(value string) error {
func init() {
flag.StringVar(&supfile, "f", "./Supfile", "Custom path to Supfile")
flag.Var(&envVars, "e", "Set environment variables")
flag.StringVar(&sshKey, "i", "", "Set the ssh key to use")
flag.Var(&envVars, "env", "Set environment variables")
flag.StringVar(&onlyHosts, "only", "", "Filter hosts using regexp")
flag.StringVar(&exceptHosts, "except", "", "Filter out hosts using regexp")
Expand Down Expand Up @@ -280,6 +282,7 @@ func main() {
}
app.Debug(debug)
app.Prefix(!disablePrefix)
app.SSHKey(sshKey)

// Run all the commands in the given network.
err = app.Run(network, commands...)
Expand Down
53 changes: 29 additions & 24 deletions ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,21 @@ import (

// Client is a wrapper over the SSH connection/sessions.
type SSHClient struct {
conn *ssh.Client
sess *ssh.Session
user string
host string
remoteStdin io.WriteCloser
remoteStdout io.Reader
remoteStderr io.Reader
connOpened bool
sessOpened bool
running bool
env string //export FOO="bar"; export BAR="baz";
color string
conn *ssh.Client
sess *ssh.Session
user string
host string
sshKeys []string // ssh key to use
remoteStdin io.WriteCloser
remoteStdout io.Reader
remoteStderr io.Reader
connOpened bool
sessOpened bool
running bool
env string //export FOO="bar"; export BAR="baz";
color string
initAuthMethodOnce sync.Once
Copy link
Collaborator

@VojtechVitek VojtechVitek Jul 22, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might slow down the init phase of sup. Imagine running 100 remote SSH connections, you'd have to init Auth method 100 times. Do we really need that?

Imho, if the identity settings is per network, we don't need to do this per-host here, do we?

Copy link
Author

@aelsabbahy aelsabbahy Jul 22, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes perfect sense, I'll re-evaluate the approach.

authMethod ssh.AuthMethod
}

type ErrConnect struct {
Expand All @@ -40,6 +43,15 @@ func (e ErrConnect) Error() string {
return fmt.Sprintf(`Connect("%v@%v"): %v`, e.User, e.Host, e.Reason)
}

func newSSHClient() *SSHClient {
return &SSHClient{
sshKeys: []string{
os.Getenv("HOME") + "/.ssh/id_rsa",
os.Getenv("HOME") + "/.ssh/id_dsa",
},
}
}

// parseHost parses and normalizes <user>@<host:port> from a given string.
func (c *SSHClient) parseHost(host string) error {
c.host = host
Expand Down Expand Up @@ -75,11 +87,8 @@ func (c *SSHClient) parseHost(host string) error {
return nil
}

var initAuthMethodOnce sync.Once
var authMethod ssh.AuthMethod

// initAuthMethod initiates SSH authentication method.
func initAuthMethod() {
func (c *SSHClient) initAuthMethod() {
var signers []ssh.Signer

// If there's a running SSH Agent, try to use its Private keys.
Expand All @@ -90,11 +99,7 @@ func initAuthMethod() {
}

// Try to read user's SSH private keys form the standard paths.
files := []string{
os.Getenv("HOME") + "/.ssh/id_rsa",
os.Getenv("HOME") + "/.ssh/id_dsa",
}
for _, file := range files {
for _, file := range c.sshKeys {
data, err := ioutil.ReadFile(file)
if err != nil {
continue
Expand All @@ -106,7 +111,7 @@ func initAuthMethod() {
signers = append(signers, signer)

}
authMethod = ssh.PublicKeys(signers...)
c.authMethod = ssh.PublicKeys(signers...)
}

// SSHDialFunc can dial an ssh server and return a client
Expand All @@ -126,7 +131,7 @@ func (c *SSHClient) ConnectWith(host string, dialer SSHDialFunc) error {
return fmt.Errorf("Already connected")
}

initAuthMethodOnce.Do(initAuthMethod)
c.initAuthMethodOnce.Do(c.initAuthMethod)

err := c.parseHost(host)
if err != nil {
Expand All @@ -136,7 +141,7 @@ func (c *SSHClient) ConnectWith(host string, dialer SSHDialFunc) error {
config := &ssh.ClientConfig{
User: c.user,
Auth: []ssh.AuthMethod{
authMethod,
c.authMethod,
},
}

Expand Down
36 changes: 32 additions & 4 deletions sup.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Stackup struct {
conf *Supfile
debug bool
prefix bool
sshKey string
}

func New(conf *Supfile) (*Stackup, error) {
Expand All @@ -27,6 +28,26 @@ func New(conf *Supfile) (*Stackup, error) {
}, nil
}

// Returns the first defined key parameter, otherwise empty string
func firstDefinedKey(keys ...string) string {
for _, key := range keys {
if key != "" {
return expandHome(key)
}
}
return ""
}

// Expands ~/foo -> /home/user/foo
func expandHome(path string) string {
if !strings.HasPrefix(path, "~/") {
return path
}
parts := strings.Split(path, "/")
parts[0] = os.Getenv("HOME")
return strings.Join(parts, "/")
}

// Run runs set of commands on multiple hosts defined by network sequentially.
// TODO: This megamoth method needs a big refactor and should be split
// to multiple smaller methods.
Expand All @@ -45,7 +66,7 @@ func (sup *Stackup) Run(network *Network, commands ...*Command) error {
// Create clients for every host (either SSH or Localhost).
var bastion *SSHClient
if network.Bastion != "" {
bastion = &SSHClient{}
bastion = newSSHClient()
if err := bastion.Connect(network.Bastion); err != nil {
return errors.Wrap(err, "connecting to bastion failed")
}
Expand Down Expand Up @@ -74,9 +95,12 @@ func (sup *Stackup) Run(network *Network, commands ...*Command) error {
}

// SSH client.
remote := &SSHClient{
env: env + `export SUP_HOST="` + host + `";`,
color: Colors[i%len(Colors)],
remote := newSSHClient()
remote.env = env + `export SUP_HOST="` + host + `";`
remote.color = Colors[i%len(Colors)]
sshKey := firstDefinedKey(sup.sshKey, network.SSHKey)
if sshKey != "" {
remote.sshKeys = []string{sshKey}
}

if bastion != nil {
Expand Down Expand Up @@ -251,3 +275,7 @@ func (sup *Stackup) Debug(value bool) {
func (sup *Stackup) Prefix(value bool) {
sup.prefix = value
}

func (sup *Stackup) SSHKey(value string) {
sup.sshKey = value
}
1 change: 1 addition & 0 deletions supfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Network struct {
Env EnvList `yaml:"env"`
Inventory string `yaml:"inventory"`
Hosts []string `yaml:"hosts"`
SSHKey string `yaml:"ssh-key"`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're trying to avoid dashes and underscores in the Supfile API. Can we think of one word here?

identity ... or sshkey ... any other suggestions?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IdentityFile is more verbose, but would match 1-1 with ~/.ssh/config syntax, thoughts?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Bastion string `yaml:"bastion"` // Jump host for the environment
}

Expand Down