diff --git a/go.mod b/go.mod index 7fb743f2..bf5b1cc3 100644 --- a/go.mod +++ b/go.mod @@ -7,5 +7,7 @@ require ( github.com/lib/pq v0.0.0-20170117205633-67c3f2a8884c github.com/sean-/postgresql-acl v0.0.0-20161225120419-d10489e5d217 github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac // indirect + github.com/xanzy/ssh-agent v0.2.1 + golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2 golang.org/x/text v0.3.0 // indirect ) diff --git a/go.sum b/go.sum index 44df3aee..9863ef57 100644 --- a/go.sum +++ b/go.sum @@ -242,6 +242,8 @@ github.com/ulikunitz/xz v0.5.4 h1:zATC2OoZ8H1TZll3FpbX+ikwmadbO699PE06cIkm9oU= github.com/ulikunitz/xz v0.5.4/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/xanzy/ssh-agent v0.2.0 h1:Adglfbi5p9Z0BmK2oKU9nTG+zKfniSfnaMYB+ULd+Ro= github.com/xanzy/ssh-agent v0.2.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnWhgSqHD8= +github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= +github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557 h1:Jpn2j6wHkC9wJv5iMfJhKqrZJx3TahFx+7sbZ7zQdxs= github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= @@ -249,6 +251,8 @@ github.com/zclconf/go-cty v0.0.0-20180302160414-49fa5e03c418 h1:uZKhc0PzQtIg+6+B github.com/zclconf/go-cty v0.0.0-20180302160414-49fa5e03c418/go.mod h1:LnDKxj8gN4aatfXUqmUNooaDjvmDcLPbAN3hYBIVoJE= golang.org/x/crypto v0.0.0-20180211211603-9de5f2eaf759 h1:6W75OzsrwJByqag5GxxtYVTVEyP+Sy+aLDUsJ9CD8OU= golang.org/x/crypto v0.0.0-20180211211603-9de5f2eaf759/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2 h1:NwxKRvbkH5MsNkvOtPZi3/3kmI8CAzs3mtv+GLQMkNo= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/net v0.0.0-20171004034648-a04bdaca5b32 h1:NjAulLPqFTaOxQu5S4qUMqscSu+mQdu+wMY0nfqSkuk= golang.org/x/net v0.0.0-20171004034648-a04bdaca5b32/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/oauth2 v0.0.0-20170928010508-bb50c06baba3 h1:YGx0PRKSN/2n/OcdFycCC0JUA/Ln+i5lPcN8VoNDus0= @@ -257,6 +261,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FY golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5 h1:x6r4Jo0KNzOOzYd8lbcRsqjuqEASK6ob3auvWYM4/8U= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0 h1:bzeyCHgoAyjZjAhvTpks+qM7sdlh4cCSitmXeCEO3B4= +golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.0.0-20171013141220-c01e4764d870/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/postgresql/config.go b/postgresql/config.go index d39fd2d9..61d7b42d 100644 --- a/postgresql/config.go +++ b/postgresql/config.go @@ -4,6 +4,7 @@ import ( "bytes" "database/sql" "fmt" + "golang.org/x/crypto/ssh" "log" "strings" "sync" @@ -87,6 +88,15 @@ type Config struct { ConnectTimeoutSec int MaxConns int ExpectedVersion semver.Version + Ssh bool + SshUser string + SshPassword string + SshPrivateKey string + SshHost string + SshHostKey string + SshPort int + SshTimeout int + SshAgent bool } // Client struct holding connection string @@ -117,17 +127,47 @@ func (c *Config) NewClient(database string) (*Client, error) { dbRegistryLock.Lock() defer dbRegistryLock.Unlock() + // TODO add ssh config to connstr dsn := c.connStr(database) dbEntry, found := dbRegistry[dsn] if !found { - db, err := sql.Open("postgres", dsn) + driverName := "postgres" + + if c.Ssh { + // Establish connection via an ssh bastion host + driverName = "postgres+ssh" + + // Configuration + sshConfig, err := prepareSSHConfig(c) + if err != nil { + return nil, err + } + + // TODO can we use the sshConfig.connection approach? + + // TODO how to support timeout? + + // Connect + sshClient, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", c.SshHost, c.SshPort), sshConfig.config) + if err != nil { + return nil, err + } + + // TODO when to close the ssh connection? + + // Register postgres+ssh + sql.Register(driverName, &postgresqlSshDriver{ + sshClient: sshClient}) + } + + db, err := sql.Open(driverName, dsn) if err != nil { return nil, errwrap.Wrapf("Error connecting to PostgreSQL server: {{err}}", err) } // We don't want to retain connection // So when we connect on a specific database which might be managed by terraform, - // we don't keep opened connection in case of the db has to be dopped in the plan. + // we don't keep opened connection in case of the db has to be dropped in the plan. db.SetMaxIdleConns(0) db.SetMaxOpenConns(c.MaxConns) @@ -179,7 +219,11 @@ func (c *Config) connStr(database string) string { "user=%s", "password=%s", "sslmode=%s", - "connect_timeout=%d", + } + + // connect_timeout is not supported when using ssh tunnel + if !c.Ssh { + dsnFmtParts = append(dsnFmtParts, "connect_timeout=%d") } if c.featureSupported(featureFallbackApplicationName) { @@ -226,7 +270,9 @@ func (c *Config) connStr(database string) string { quote(c.Username), quote(""), quote(c.SSLMode), - c.ConnectTimeoutSec, + } + if !c.Ssh { + logValues = append(logValues, c.ConnectTimeoutSec) } if c.featureSupported(featureFallbackApplicationName) { logValues = append(logValues, quote(c.ApplicationName)) @@ -245,7 +291,9 @@ func (c *Config) connStr(database string) string { quote(c.Username), quote(c.Password), quote(c.SSLMode), - c.ConnectTimeoutSec, + } + if !c.Ssh { + connValues = append(connValues, c.ConnectTimeoutSec) } if c.featureSupported(featureFallbackApplicationName) { connValues = append(connValues, quote(c.ApplicationName)) diff --git a/postgresql/provider.go b/postgresql/provider.go index c200ae94..6f0516a4 100644 --- a/postgresql/provider.go +++ b/postgresql/provider.go @@ -2,6 +2,7 @@ package postgresql import ( "fmt" + "time" "github.com/blang/semver" "github.com/hashicorp/errwrap" @@ -11,7 +12,16 @@ import ( const ( defaultProviderMaxOpenConnections = uint(4) - defaultExpectedPostgreSQLVersion = "9.0.0" + + defaultExpectedPostgreSQLVersion = "9.0.0" + + defaultSshUser = "root" + + // defaultSshPort is used if there is no port given + defaultSshPort = 22 + + // defaultSshTimeout is used if there is no timeout given + defaultSshTimeout = 5 * time.Minute ) // Provider returns a terraform.ResourceProvider. @@ -49,7 +59,7 @@ func Provider() terraform.ResourceProvider { Description: "Password to be used if the PostgreSQL server demands password authentication", Sensitive: true, }, - // Conection username can be different than database username with user name mapas (e.g.: in Azure) + // Connection username can be different than database username with user name maps (e.g.: in Azure) // See https://www.postgresql.org/docs/current/auth-username-maps.html "database_username": { Type: schema.TypeString, @@ -97,6 +107,60 @@ func Provider() terraform.ResourceProvider { Description: "Specify the expected version of PostgreSQL.", ValidateFunc: validateExpectedVersion, }, + "connection": { + Type: schema.TypeList, + Optional: true, + Description: "", // TODO + MaxItems: 1, + // TODO validate the connection configuration + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "bastion_user": { + Type: schema.TypeString, + Optional: true, + Default: defaultSshUser, + Description: "The user for the connection to the bastion host. Defaults to the value of the user field.", + }, + "bastion_password": { + Type: schema.TypeString, + Optional: true, + Description: "", + }, + "bastion_private_key": { + Type: schema.TypeString, + Optional: true, + Description: "The contents of an SSH key file to use for the bastion host.", + }, + "bastion_host": { + Type: schema.TypeString, + Required: true, + Description: "Setting this enables the bastion Host connection. This host will be connected to first, and then the host connection will be made from there.", + }, + "bastion_host_key": { + Type: schema.TypeString, + Optional: true, + Description: "The public key from the remote host or the signing CA, used to verify the host connection.", + }, + "bastion_port": { + Type: schema.TypeInt, + Optional: true, + Default: defaultSshPort, + Description: "The port to use connect to the bastion host. Defaults to the value of the port field.", + }, + "timeout": { + Type: schema.TypeString, + Optional: true, + Description: "The timeout to wait for the connection to become available. This defaults to 5 minutes.", + }, + "agent": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Set to false to disable using ssh-agent to authenticate.", + }, + }, + }, + }, }, ResourcesMap: map[string]*schema.Resource{ @@ -162,6 +226,26 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { ExpectedVersion: version, } + // TODO configure using a hashset? + + if conns, ok := d.Get("connection").([]interface{}); ok && len(conns) == 1 { + conn := conns[0].(map[string]interface{}) + + config.SshUser = conn["bastion_user"].(string) + config.SshPassword = conn["bastion_password"].(string) + config.SshPrivateKey = conn["bastion_private_key"].(string) + config.SshHost = conn["bastion_host"].(string) + config.SshHostKey = conn["bastion_host_key"].(string) + config.SshPort = conn["bastion_port"].(int) + + // TODO allow configure timeout (with correct parsing) + //config.Timeout = conn["timeout"].(int) + + config.SshAgent = conn["agent"].(bool) + + config.Ssh = config.SshHost != "" + } + client, err := config.NewClient(d.Get("database").(string)) if err != nil { return nil, errwrap.Wrapf("Error initializing PostgreSQL client: {{err}}", err) diff --git a/postgresql/provider_test.go b/postgresql/provider_test.go index 3c657b45..45e73c88 100644 --- a/postgresql/provider_test.go +++ b/postgresql/provider_test.go @@ -1,20 +1,24 @@ package postgresql import ( - "os" - "testing" - + "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" + "os" + "testing" ) var testAccProviders map[string]terraform.ResourceProvider var testAccProvider *schema.Provider +var testAccSshProvider *schema.Provider func init() { testAccProvider = Provider().(*schema.Provider) + testAccSshProvider = Provider().(*schema.Provider) + testAccProviders = map[string]terraform.ResourceProvider{ - "postgresql": testAccProvider, + "postgresql": testAccProvider, + "postgresql+ssh": testAccSshProvider, } } @@ -42,3 +46,67 @@ func testAccPreCheck(t *testing.T) { t.Fatal(err) } } + +func testAccPreCheckSsh(t *testing.T) { + //var host string + //if host = os.Getenv("PGHOST"); host == "" { + // t.Fatal("PGHOST must be set for acceptance tests") + //} + + if v := os.Getenv("PGUSER"); v == "" { + t.Fatal("PGUSER must be set for acceptance tests") + } + + bastionPrivateKey := ` +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn +NhAAAAAwEAAQAAAQEAu5wYi5SxCTcmChVWaS34MYV25GC0eyrhPLt54lmBNHmA+088a038 +azBMs8/XxYcMpcIqE92UBXuMXRe230leV36t0Qw0n3/eg/OP9ctSCOD1pISjsLFSi5UTp9 +dbUlePYbYmbsRm14+3vhGXlOLUc6ApfdO4wn13NSi/zQVrEtlz15GUNWmgFKPfFHNTlQDO +QKKpMgNKeUxbeq1kfrA4b/9PwZyaQcQn84SqNAOYTuy8ZqnC7A6yRJFLfnqxOu6TYUkO3V +/SH7oivh02bCgKMCTi1R8z0qLwz+vn+xRYb3ysDxUzqMSoZ8rLBXjnrF9xv3Uoq0b6bfbK +lmhywslkxwAAA9AXdYZFF3WGRQAAAAdzc2gtcnNhAAABAQC7nBiLlLEJNyYKFVZpLfgxhX +bkYLR7KuE8u3niWYE0eYD7TzxrTfxrMEyzz9fFhwylwioT3ZQFe4xdF7bfSV5Xfq3RDDSf +f96D84/1y1II4PWkhKOwsVKLlROn11tSV49htiZuxGbXj7e+EZeU4tRzoCl907jCfXc1KL +/NBWsS2XPXkZQ1aaAUo98Uc1OVAM5AoqkyA0p5TFt6rWR+sDhv/0/BnJpBxCfzhKo0A5hO +7LxmqcLsDrJEkUt+erE67pNhSQ7dX9IfuiK+HTZsKAowJOLVHzPSovDP6+f7FFhvfKwPFT +OoxKhnyssFeOesX3G/dSirRvpt9sqWaHLCyWTHAAAAAwEAAQAAAQEArrpRjeYc/9UyA2Ae +C3V52z1PHqIGVVP5VGPSv4HWuPWUr/n67oFCXt4sAafIcLo3iEWOhNPwIS8Q6j7E3a5qRB +jCb5jrhcVEiyYTZLtJGuXRQbka7twnYcKk/MOw1L6h1kIcBzu6AHdkjIu73jln3oxDOGIw +iErr9EGQaLTsJS9xXFD7R8opqNNTb7uQHGbDux5TWXSLNRtUhi/m/i+tcPBf+edhT/I0lP +9msEzIxhCjr+1/M9yQgsLIs2pyXYRlkBs2J3ZIo5PuF+SclOC7YudorA8g/KbX1nbTK84Z +MIrvBIjQxcPQ9rGHow2tOy2fQDsErj9H61RrvjkCIWC3KQAAAIALFVGwKDQX82jAOjaLAy +NUCYPYQPJ3XfSITyh/59SOexDkNxY2IpSx1cD6FrJUkqbKAbJ9PgqA18aAauQnaMpIUJ4Y +128iVH7H2AgrQPFNpgbRj7lIsn6Y9W6Uj5PA3jQHepypaT3S+F4HhcD15S7V2bi9olBmYv +2O+fKSNH47KwAAAIEA7uhpTBM9C6CXKoFu2FISKLaJ63gk0Z32ezyRyv2KH2ctJV1psuqd +MiaihieGg+6tXuth/EoMWteagDG+845G/BA4OOMV1vidAf2MhzmkO4XPRGJ3eFIaHhXl5i +mIhcDsJWQbZZ3lInWOyGgP2GmLt/1WlBVa/ueYfnkXeMmYEysAAACBAMkIKV6oPTmGCu8w +CxaFi27NPbGtVlfVUqxTLXUqb4KR6I3xdzNkX89dMUciY3ty0U6KHlyvuZ8NbHOS0DFRAj +kEUUUYbBzkLkM1RxzAWiy+BueGdxNZnPuFqi5qsgguHPhMe0+2H4TlQn9J3pRK5bM3SoK2 +j6FW0DcOmqu981bVAAAAGmxla3NlQERvbWluaWtzLU1CUC0yLmxvY2Fs +-----END OPENSSH PRIVATE KEY----- +` + + c := map[string]interface{}{ + "host": "postgres", + "port": 5432, + "connection": []map[string]interface{}{ + { + "bastion_host": "localhost", + "bastion_port": 20022, + "bastion_user": "test", + "bastion_private_key": bastionPrivateKey, + }, + }, + } + + rc, err := config.NewRawConfig(c) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = testAccSshProvider.Configure(terraform.NewResourceConfig(rc)) + if err != nil { + t.Fatal(err) + } +} diff --git a/postgresql/ssh.go b/postgresql/ssh.go new file mode 100644 index 00000000..32eced32 --- /dev/null +++ b/postgresql/ssh.go @@ -0,0 +1,410 @@ +package postgresql + +import ( + "bytes" + "database/sql/driver" + "encoding/pem" + "errors" + "fmt" + "github.com/lib/pq" + "github.com/xanzy/ssh-agent" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" + "golang.org/x/crypto/ssh/knownhosts" + "io/ioutil" + "log" + "net" + "os" + "path/filepath" + "strings" + "time" +) + +type postgresqlSshDriver struct { + sshClient *ssh.Client +} + +// postgresqlSshDriver implements database/sql/driver.Driver interface +var _ driver.Driver = &postgresqlSshDriver{} + +// postgresqlSshDriver implements github.com/lib/pq.Dialer interface +var _ pq.Dialer = &postgresqlSshDriver{} + +func (p *postgresqlSshDriver) Open(name string) (driver.Conn, error) { + return pq.DialOpen(p, name) +} + +func (p *postgresqlSshDriver) Dial(network string, address string) (net.Conn, error) { + return p.sshClient.Dial(network, address) +} + +func (p *postgresqlSshDriver) DialTimeout(network string, address string, timeout time.Duration) (net.Conn, error) { + // TODO how to correctly implement dial timeout? + return p.sshClient.Dial(network, address) +} + +type sshConfig struct { + // The configuration of the Go SSH connection + config *ssh.ClientConfig + + // connection returns a new connection. The current connection + // in use will be closed as part of the Close method, or in the + // case an error occurs. + // TODO can this pattern be reused? + //connection func() (net.Conn, error) + + // noPty, if true, will not request a pty from the remote end. + //noPty bool + + // sshAgent is a struct surrounding the agent.Agent client and the net.Conn + // to the SSH Agent. It is nil if no SSH agent is configured + sshAgent *sshAgent +} + +// prepareSSHConfig is used to turn the *postgresql.Config provided into a +// usable *SSHConfig for client initialization. +func prepareSSHConfig(config *Config) (*sshConfig, error) { + sshAgent, err := connectToAgent(config) + if err != nil { + return nil, err + } + + host := fmt.Sprintf("%s:%d", config.Host, config.Port) + + sshConf, err := buildSSHClientConfig(sshClientConfigOpts{ + user: config.SshUser, + host: host, + privateKey: config.SshPrivateKey, + password: config.Password, + hostKey: config.SshHostKey, + // TODO remove support for a certificate + //certificate: config.Certificate, + sshAgent: sshAgent, + }) + if err != nil { + return nil, err + } + + // TODO investigate what connect func does? + + //connectFunc := ConnectFunc("tcp", host) + // + //var bastionConf *ssh.ClientConfig + //if config.BastionHost != "" { + // bastionHost := fmt.Sprintf("%s:%d", config.BastionHost, config.BastionPort) + // + // bastionConf, err = buildSSHClientConfig(sshClientConfigOpts{ + // user: config.BastionUser, + // host: bastionHost, + // privateKey: config.BastionPrivateKey, + // password: config.BastionPassword, + // hostKey: config.HostKey, + // sshAgent: sshAgent, + // }) + // if err != nil { + // return nil, err + // } + // + // connectFunc = BastionConnectFunc("tcp", bastionHost, bastionConf, "tcp", host) + //} + + sshConfig := &sshConfig{ + config: sshConf, + //connection: connectFunc, + sshAgent: sshAgent, + } + return sshConfig, nil +} + +type sshClientConfigOpts struct { + privateKey string + password string + sshAgent *sshAgent + certificate string + user string + host string + hostKey string +} + +// safeDuration returns either the parsed duration or a default value +func safeDuration(dur string, defaultDur time.Duration) time.Duration { + d, err := time.ParseDuration(dur) + if err != nil { + log.Printf("Invalid duration '%s', using default of %s", dur, defaultDur) + return defaultDur + } + return d +} + +func buildSSHClientConfig(opts sshClientConfigOpts) (*ssh.ClientConfig, error) { + hkCallback := ssh.InsecureIgnoreHostKey() + + if opts.hostKey != "" { + // The knownhosts package only takes paths to files, but terraform + // generally wants to handle config data in-memory. Rather than making + // the known_hosts file an exception, write out the data to a temporary + // file to create the HostKeyCallback. + tf, err := ioutil.TempFile("", "tf-known_hosts") + if err != nil { + return nil, fmt.Errorf("failed to create temp known_hosts file: %s", err) + } + defer tf.Close() + defer os.RemoveAll(tf.Name()) + + // we mark this as a CA as well, but the host key fallback will still + // use it as a direct match if the remote host doesn't return a + // certificate. + if _, err := tf.WriteString(fmt.Sprintf("@cert-authority %s %s\n", opts.host, opts.hostKey)); err != nil { + return nil, fmt.Errorf("failed to write temp known_hosts file: %s", err) + } + tf.Sync() + + hkCallback, err = knownhosts.New(tf.Name()) + if err != nil { + return nil, err + } + } + + conf := &ssh.ClientConfig{ + HostKeyCallback: hkCallback, + User: opts.user, + } + + if opts.privateKey != "" { + //if opts.certificate != "" { + // log.Println("using client certificate for authentication") + // + // certSigner, err := signCertWithPrivateKey(opts.privateKey, opts.certificate) + // if err != nil { + // return nil, err + // } + // conf.Auth = append(conf.Auth, certSigner) + //} else { + log.Println("using private key for authentication") + + pubKeyAuth, err := readPrivateKey(opts.privateKey) + if err != nil { + return nil, err + } + conf.Auth = append(conf.Auth, pubKeyAuth) + //} + } + + if opts.password != "" { + conf.Auth = append(conf.Auth, ssh.Password(opts.password)) + conf.Auth = append(conf.Auth, ssh.KeyboardInteractive( + PasswordKeyboardInteractive(opts.password))) + } + + if opts.sshAgent != nil { + conf.Auth = append(conf.Auth, opts.sshAgent.Auth()) + } + + return conf, nil +} + +func readPrivateKey(pk string) (ssh.AuthMethod, error) { + // We parse the private key on our own first so that we can + // show a nicer error if the private key has a password. + block, _ := pem.Decode([]byte(pk)) + if block == nil { + return nil, errors.New("Failed to read ssh private key: no key found") + } + if block.Headers["Proc-Type"] == "4,ENCRYPTED" { + return nil, errors.New( + "Failed to read ssh private key: password protected keys are\n" + + "not supported. Please decrypt the key prior to use.") + } + + signer, err := ssh.ParsePrivateKey([]byte(pk)) + if err != nil { + return nil, fmt.Errorf("Failed to parse ssh private key: %s", err) + } + + return ssh.PublicKeys(signer), nil +} + +func connectToAgent(config *Config) (*sshAgent, error) { + // TODO support agent argument for the provider + //if config.Agent != true { + // // No agent configured + // return nil, nil + //} + + agent, conn, err := sshagent.New() + if err != nil { + return nil, err + } + + // connection close is handled over in Communicator + return &sshAgent{ + agent: agent, + conn: conn, + // TODO support AgentIdentity argument + //id: config.AgentIdentity, + }, nil +} + +// A tiny wrapper around an agent.Agent to expose the ability to close its +// associated connection on request. +type sshAgent struct { + agent agent.Agent + conn net.Conn + id string +} + +func (a *sshAgent) Close() error { + if a.conn == nil { + return nil + } + + return a.conn.Close() +} + +// make an attempt to either read the identity file or find a corresponding +// public key file using the typical openssh naming convention. +// This returns the public key in wire format, or nil when a key is not found. +func findIDPublicKey(id string) []byte { + for _, d := range idKeyData(id) { + signer, err := ssh.ParsePrivateKey(d) + if err == nil { + log.Println("[DEBUG] parsed id private key") + pk := signer.PublicKey() + return pk.Marshal() + } + + // try it as a publicKey + pk, err := ssh.ParsePublicKey(d) + if err == nil { + log.Println("[DEBUG] parsed id public key") + return pk.Marshal() + } + + // finally try it as an authorized key + pk, _, _, _, err = ssh.ParseAuthorizedKey(d) + if err == nil { + log.Println("[DEBUG] parsed id authorized key") + return pk.Marshal() + } + } + + return nil +} + +// Try to read an id file using the id as the file path. Also read the .pub +// file if it exists, as the id file may be encrypted. Return only the file +// data read. We don't need to know what data came from which path, as we will +// try parsing each as a private key, a public key and an authorized key +// regardless. +func idKeyData(id string) [][]byte { + idPath, err := filepath.Abs(id) + if err != nil { + return nil + } + + var fileData [][]byte + + paths := []string{idPath} + + if !strings.HasSuffix(idPath, ".pub") { + paths = append(paths, idPath+".pub") + } + + for _, p := range paths { + d, err := ioutil.ReadFile(p) + if err != nil { + log.Printf("[DEBUG] error reading %q: %s", p, err) + continue + } + log.Printf("[DEBUG] found identity data at %q", p) + fileData = append(fileData, d) + } + + return fileData +} + +// sortSigners moves a signer with an agent comment field matching the +// agent_identity to the head of the list when attempting authentication. This +// helps when there are more keys loaded in an agent than the host will allow +// attempts. +func (s *sshAgent) sortSigners(signers []ssh.Signer) { + if s.id == "" || len(signers) < 2 { + return + } + + // if we can locate the public key, either by extracting it from the id or + // locating the .pub file, then we can more easily determine an exact match + idPk := findIDPublicKey(s.id) + + // if we have a signer with a connect field that matches the id, send that + // first, otherwise put close matches at the front of the list. + head := 0 + for i := range signers { + pk := signers[i].PublicKey() + k, ok := pk.(*agent.Key) + if !ok { + continue + } + + // check for an exact match first + if bytes.Equal(pk.Marshal(), idPk) || s.id == k.Comment { + signers[0], signers[i] = signers[i], signers[0] + break + } + + // no exact match yet, move it to the front if it's close. The agent + // may have loaded as a full filepath, while the config refers to it by + // filename only. + if strings.HasSuffix(k.Comment, s.id) { + signers[head], signers[i] = signers[i], signers[head] + head++ + continue + } + } + + ss := []string{} + for _, signer := range signers { + pk := signer.PublicKey() + k := pk.(*agent.Key) + ss = append(ss, k.Comment) + } +} + +func (s *sshAgent) Signers() ([]ssh.Signer, error) { + signers, err := s.agent.Signers() + if err != nil { + return nil, err + } + + s.sortSigners(signers) + return signers, nil +} + +func (a *sshAgent) Auth() ssh.AuthMethod { + return ssh.PublicKeysCallback(a.Signers) +} + +func (a *sshAgent) ForwardToAgent(client *ssh.Client) error { + return agent.ForwardToAgent(client, a.agent) +} + +// An implementation of ssh.KeyboardInteractiveChallenge that simply sends +// back the password for all questions. The questions are logged. +func PasswordKeyboardInteractive(password string) ssh.KeyboardInteractiveChallenge { + return func(user, instruction string, questions []string, echos []bool) ([]string, error) { + log.Printf("Keyboard interactive challenge: ") + log.Printf("-- User: %s", user) + log.Printf("-- Instructions: %s", instruction) + for i, question := range questions { + log.Printf("-- Question %d: %s", i+1, question) + } + + // Just send the password back for all questions + answers := make([]string, len(questions)) + for i := range answers { + answers[i] = string(password) + } + + return answers, nil + } +} diff --git a/postgresql/ssh_test.go b/postgresql/ssh_test.go new file mode 100644 index 00000000..37c5aa25 --- /dev/null +++ b/postgresql/ssh_test.go @@ -0,0 +1,37 @@ +package postgresql + +import ( + "github.com/hashicorp/terraform/helper/resource" + "testing" +) + +func TestAccPostgresqlSsh_Connect(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckSsh(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: "", + Check: resource.ComposeTestCheckFunc(), + }, + }, + }) +} + +func TestAccPostgresqlSshDatabase_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckSsh(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckPostgresqlDatabaseDestroy, + Steps: []resource.TestStep{ + { + Config: testAccPostgreSQLDatabaseConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckPostgresqlDatabaseExists("postgresql_database.mydb"), + ), + }, + }, + }) +} + +// TODO add documentation of the ssh tunnel feature diff --git a/postgresql/utils_test.go b/postgresql/utils_test.go index da99ebd0..c15f2a7d 100644 --- a/postgresql/utils_test.go +++ b/postgresql/utils_test.go @@ -26,15 +26,15 @@ func testCheckCompatibleVersion(t *testing.T, feature featureName) { } } -func getTestConfig(t *testing.T) Config { - getEnv := func(key, fallback string) string { - value := os.Getenv(key) - if len(value) == 0 { - return fallback - } - return value +func getEnv(key, fallback string) string { + value := os.Getenv(key) + if len(value) == 0 { + return fallback } + return value +} +func getTestConfig(t *testing.T) Config { dbPort, err := strconv.Atoi(getEnv("PGPORT", "5432")) if err != nil { t.Fatalf("could not cast PGPORT value as integer: %v", err) @@ -49,6 +49,21 @@ func getTestConfig(t *testing.T) Config { } } +func getTestSshConfig(t *testing.T) Config { + dbPort, err := strconv.Atoi(getEnv("PGPORT", "5432")) + if err != nil { + t.Fatalf("could not cast PGPORT value as integer: %v", err) + } + // TODO define variable names + return Config{ + Host: getEnv("PGHOST", "localhost"), + Port: dbPort, + Username: getEnv("PGUSER", ""), + Password: getEnv("PGPASSWORD", ""), + SSLMode: getEnv("PGSSLMODE", ""), + } +} + func skipIfNotAcc(t *testing.T) { if os.Getenv(resource.TestEnvVar) == "" { t.Skip(fmt.Sprintf( diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index d580a869..8dd7575b 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -1,9 +1,18 @@ version: "3" services: - postgres: - image: postgres:${PGVERSION:-latest} - environment: - POSTGRES_PASSWORD: ${PGPASSWORD} - ports: - - 25432:5432 + postgres: + image: postgres:${PGVERSION:-latest} + environment: + POSTGRES_PASSWORD: ${PGPASSWORD} + ports: + - "25432:5432" + ssh: + image: panubo/sshd:1.0.1 + environment: + SSH_USERS: test:1001:1001 + ports: + - "20022:22" + volumes: + - ./ssh-keys/test.pub:/etc/authorized_keys/test + - ./ssh-host-keys:/etc/ssh/keys/ diff --git a/tests/ssh-host-keys/ssh_host_dsa_key b/tests/ssh-host-keys/ssh_host_dsa_key new file mode 100644 index 00000000..db7ca56e --- /dev/null +++ b/tests/ssh-host-keys/ssh_host_dsa_key @@ -0,0 +1,12 @@ +-----BEGIN DSA PRIVATE KEY----- +MIIBugIBAAKBgQCWhRoStjPgeSpqsVDJp+8vB+xSoZ/PIskbRze/LgIDqLzPpIDk +mKFjVrTMjoC5TNjt4pRcJz7IPLUxkyAAqqBilRjCZOHClxis0ot9yLQI4wygGtaX +X2kvT0o43jrAO3VFobPbV4g09bMHo5hr6eKRdi6ma5TKjeGH/45EIRQ3UwIVAMRL +AI6C4EUq8bX4MUOsm3JRtFa7AoGAWK230MCbNjBqRCTafZAQafS1c2aEYvCOwiN+ +cYONOJYp6BN7f49q1rRsn6/PoQhQeleKlxgahvu2V1jddwjxcN+SGJB+Uao8qZFb +dDwGUktpyWiA6p/12c6HWoN4toLhfILYprC4TZvyzuycQGDOMZ8hh7+Hr+H9BmES +gxJWYFsCgYBzyw4MDrDc8BGoNMuqulo84QIka17tk19blyd9B7OQRdbL47Wrawcg +95GPA5CWx91afO31l8qozYsKdH6U4fKF+RWB1wr24wQboVJOSvmxfQ2L1iQHSrd7 +8efyt2VLs2rdoMfo24EQyLXyExGnzhQp3dtUCMGEcwvhdreZFBGx/AIUNviaoAJN +lji9cKvQ7LpMZBBxMR4= +-----END DSA PRIVATE KEY----- diff --git a/tests/ssh-host-keys/ssh_host_dsa_key.pub b/tests/ssh-host-keys/ssh_host_dsa_key.pub new file mode 100644 index 00000000..65622a0d --- /dev/null +++ b/tests/ssh-host-keys/ssh_host_dsa_key.pub @@ -0,0 +1 @@ +ssh-dss AAAAB3NzaC1kc3MAAACBAJaFGhK2M+B5KmqxUMmn7y8H7FKhn88iyRtHN78uAgOovM+kgOSYoWNWtMyOgLlM2O3ilFwnPsg8tTGTIACqoGKVGMJk4cKXGKzSi33ItAjjDKAa1pdfaS9PSjjeOsA7dUWhs9tXiDT1swejmGvp4pF2LqZrlMqN4Yf/jkQhFDdTAAAAFQDESwCOguBFKvG1+DFDrJtyUbRWuwAAAIBYrbfQwJs2MGpEJNp9kBBp9LVzZoRi8I7CI35xg404linoE3t/j2rWtGyfr8+hCFB6V4qXGBqG+7ZXWN13CPFw35IYkH5RqjypkVt0PAZSS2nJaIDqn/XZzodag3i2guF8gtimsLhNm/LO7JxAYM4xnyGHv4ev4f0GYRKDElZgWwAAAIBzyw4MDrDc8BGoNMuqulo84QIka17tk19blyd9B7OQRdbL47Wrawcg95GPA5CWx91afO31l8qozYsKdH6U4fKF+RWB1wr24wQboVJOSvmxfQ2L1iQHSrd78efyt2VLs2rdoMfo24EQyLXyExGnzhQp3dtUCMGEcwvhdreZFBGx/A== root@728667996a13 diff --git a/tests/ssh-host-keys/ssh_host_ecdsa_key b/tests/ssh-host-keys/ssh_host_ecdsa_key new file mode 100644 index 00000000..e7d7aacd --- /dev/null +++ b/tests/ssh-host-keys/ssh_host_ecdsa_key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIHbORZ4+S7ToYUX8aXIdF98UzhWb2UFElaPi2HZG5m7AoAoGCCqGSM49 +AwEHoUQDQgAEWL/YMYqoNZUBMYJqFTzTyhZYrr3t75WPhoudvelqRQZOx8dTaI5F +fkp7V9ViL8LHOysDvtvM1GWzTJf/+5fVIg== +-----END EC PRIVATE KEY----- diff --git a/tests/ssh-host-keys/ssh_host_ecdsa_key.pub b/tests/ssh-host-keys/ssh_host_ecdsa_key.pub new file mode 100644 index 00000000..0a9c92a5 --- /dev/null +++ b/tests/ssh-host-keys/ssh_host_ecdsa_key.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFi/2DGKqDWVATGCahU808oWWK697e+Vj4aLnb3pakUGTsfHU2iORX5Ke1fVYi/CxzsrA77bzNRls0yX//uX1SI= root@728667996a13 diff --git a/tests/ssh-host-keys/ssh_host_ed25519_key b/tests/ssh-host-keys/ssh_host_ed25519_key new file mode 100644 index 00000000..24c50b6b --- /dev/null +++ b/tests/ssh-host-keys/ssh_host_ed25519_key @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACBjrcNCfJVCkBTouZlViigIOXMBm/lCALrf5nTbm1770gAAAJjiEjdb4hI3 +WwAAAAtzc2gtZWQyNTUxOQAAACBjrcNCfJVCkBTouZlViigIOXMBm/lCALrf5nTbm1770g +AAAEDrztVXmrfXClBxbI2kjqneNUUG3v50dmX9kzKGbLGz8WOtw0J8lUKQFOi5mVWKKAg5 +cwGb+UIAut/mdNubXvvSAAAAEXJvb3RANzI4NjY3OTk2YTEzAQIDBA== +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/ssh-host-keys/ssh_host_ed25519_key.pub b/tests/ssh-host-keys/ssh_host_ed25519_key.pub new file mode 100644 index 00000000..4461b346 --- /dev/null +++ b/tests/ssh-host-keys/ssh_host_ed25519_key.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGOtw0J8lUKQFOi5mVWKKAg5cwGb+UIAut/mdNubXvvS root@728667996a13 diff --git a/tests/ssh-host-keys/ssh_host_rsa_key b/tests/ssh-host-keys/ssh_host_rsa_key new file mode 100644 index 00000000..3e6fcb57 --- /dev/null +++ b/tests/ssh-host-keys/ssh_host_rsa_key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAw8mjREwmqTQIZmG2f4yeXe7cCz7ncrYhSrs81eP0HZcsEoNZ +Y0q6dQmXqxkV94p3SOGyCCCVzOoBBaxuqNFUslK8q1KgkBzeAcVTH9Ycx/A6dY3a +GG5swhdkbInmif1tI+R/NtEU/Fm5Xk21c8YUH8iAwN42lTVf0t0q/ZBc32rk0XSh +OKIDsspiFU70DefO94Yn21tLsmEckXcQ3G6Z9NZ8K3Wo1JO20QEsa6RWv9oiBZUf +ylM3DnJhK48tMzXHaCdEougqvpDBbbHPCo/4t9Il3yKHaCkDz05tofxCXGXeZdwD +FKleMMdZ2TeWjkQ01YdLTdGo8lmwCwPSDyGDqwIDAQABAoIBADEgGvM8GEdEYwct +ZVlDs0jmchfwRKqnwFq7+FvCwrHaCJjslDUxvu825PNta/GcsKl81+rqIiw0WuVN +isaZH6NO10A1j0wZsirHlD/cvYP9Zu2wFhUjP+7DZ9NEFrBxAj2LS9A5TgazjKt4 +3BqcjNgcuxGpsBBoQA7sp6jP3D3CZ8HXLS7gSo2vX6YjLD/+eQqCopiP6t6C+Grf +BimFmeeL3Xel77vv7xcasM2uYeZ08JnsnfaOXs0eTRGGmjvYuZouSRKoTUzd4VTk +A0i3xrlQAyvikuEpLOCWzmcKWXPIaKheNBbFVurr7lo0Z0A83TU3W3sdurQHU3DN +7ym4XQECgYEA82B1zpeCjyqL35VlJYBRnJV/NEJdMki/F3L4nTviigryOSuIb1nZ +8mAwSW3Q0c86qL+UtnvS8ThEreD4/ptgLOIJWVYR/kiC43CWx4p0HGHENk5Ijg5d +ZCl13MyTGLXzHWy5AVsUTc2kBDEg+7oE8RiaaWLbbbCp43KrS4VJfuECgYEAzfFK +z0wJU49AO4xi04w8kkQ0NL8gS6DW59KX17COAGPa18Zliyap5zQStabYUhzqlp8I +EpMem/d6EvGa6x1MpSCNlZlo4xYrCdjnlwahgUcRCrfOeRMdqpSKDng9gJxkvFpc +ahPauFSlhoG/hqSbd4QP7kV8hhSCCZ6dJlVrEAsCgYA+hZU+EWYU6VUthu+JBsHw +e+dFwZa3iECvAXYkznGQDOfVD/3ovShkP5moA3IVtCrZlv3ZM04pcc8S7CyLG9dF +MHw+WwIcVPxq+U2CzWqur978JHg3JjGPvabtphBT1Moz0O5mDsPUiPONsCFNCaij +VzKzyBWexDegmqCusfsDgQKBgHHa3IkOeHmB0Pka++gIt9QFcPdYUvp8yVMQ0nGk +Yl1E11BDlw//KB9yYoWa4C1FX0w2T7g1Lc78Wrjuab9iS2VfQedbEOm678BZ8m9E +czWNnJZYWAYH03bi+BBX2WipDegz7LOYlmsiIQDj6ob9qhXBJS2NrPJTlDDNSARR +ZQdRAoGBAJjvCu3FXkb0TcLg+yaQHt3dqopec1FzOJlOySPtDbb8SKVC8zIYPlnA +VWj8JgOWN3zvcjcqwFHpQEGfDEu9w9UWolV1kjBt/LtqRHDoMoClp2ac8OO8eZ1W +zY1hN+tfvTp+2+/9bfrEFCXBkS5eB8Kso625YeJwJrXT/dF5LWeP +-----END RSA PRIVATE KEY----- diff --git a/tests/ssh-host-keys/ssh_host_rsa_key.pub b/tests/ssh-host-keys/ssh_host_rsa_key.pub new file mode 100644 index 00000000..b0b63080 --- /dev/null +++ b/tests/ssh-host-keys/ssh_host_rsa_key.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDyaNETCapNAhmYbZ/jJ5d7twLPudytiFKuzzV4/QdlywSg1ljSrp1CZerGRX3indI4bIIIJXM6gEFrG6o0VSyUryrUqCQHN4BxVMf1hzH8Dp1jdoYbmzCF2RsieaJ/W0j5H820RT8WbleTbVzxhQfyIDA3jaVNV/S3Sr9kFzfauTRdKE4ogOyymIVTvQN5873hifbW0uyYRyRdxDcbpn01nwrdajUk7bRASxrpFa/2iIFlR/KUzcOcmErjy0zNcdoJ0Si6Cq+kMFtsc8Kj/i30iXfIodoKQPPTm2h/EJcZd5l3AMUqV4wx1nZN5aORDTVh0tN0ajyWbALA9IPIYOr root@728667996a13 diff --git a/tests/ssh-keys/test.key b/tests/ssh-keys/test.key new file mode 100644 index 00000000..c7728ed6 --- /dev/null +++ b/tests/ssh-keys/test.key @@ -0,0 +1,27 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn +NhAAAAAwEAAQAAAQEAu5wYi5SxCTcmChVWaS34MYV25GC0eyrhPLt54lmBNHmA+088a038 +azBMs8/XxYcMpcIqE92UBXuMXRe230leV36t0Qw0n3/eg/OP9ctSCOD1pISjsLFSi5UTp9 +dbUlePYbYmbsRm14+3vhGXlOLUc6ApfdO4wn13NSi/zQVrEtlz15GUNWmgFKPfFHNTlQDO +QKKpMgNKeUxbeq1kfrA4b/9PwZyaQcQn84SqNAOYTuy8ZqnC7A6yRJFLfnqxOu6TYUkO3V +/SH7oivh02bCgKMCTi1R8z0qLwz+vn+xRYb3ysDxUzqMSoZ8rLBXjnrF9xv3Uoq0b6bfbK +lmhywslkxwAAA9AXdYZFF3WGRQAAAAdzc2gtcnNhAAABAQC7nBiLlLEJNyYKFVZpLfgxhX +bkYLR7KuE8u3niWYE0eYD7TzxrTfxrMEyzz9fFhwylwioT3ZQFe4xdF7bfSV5Xfq3RDDSf +f96D84/1y1II4PWkhKOwsVKLlROn11tSV49htiZuxGbXj7e+EZeU4tRzoCl907jCfXc1KL +/NBWsS2XPXkZQ1aaAUo98Uc1OVAM5AoqkyA0p5TFt6rWR+sDhv/0/BnJpBxCfzhKo0A5hO +7LxmqcLsDrJEkUt+erE67pNhSQ7dX9IfuiK+HTZsKAowJOLVHzPSovDP6+f7FFhvfKwPFT +OoxKhnyssFeOesX3G/dSirRvpt9sqWaHLCyWTHAAAAAwEAAQAAAQEArrpRjeYc/9UyA2Ae +C3V52z1PHqIGVVP5VGPSv4HWuPWUr/n67oFCXt4sAafIcLo3iEWOhNPwIS8Q6j7E3a5qRB +jCb5jrhcVEiyYTZLtJGuXRQbka7twnYcKk/MOw1L6h1kIcBzu6AHdkjIu73jln3oxDOGIw +iErr9EGQaLTsJS9xXFD7R8opqNNTb7uQHGbDux5TWXSLNRtUhi/m/i+tcPBf+edhT/I0lP +9msEzIxhCjr+1/M9yQgsLIs2pyXYRlkBs2J3ZIo5PuF+SclOC7YudorA8g/KbX1nbTK84Z +MIrvBIjQxcPQ9rGHow2tOy2fQDsErj9H61RrvjkCIWC3KQAAAIALFVGwKDQX82jAOjaLAy +NUCYPYQPJ3XfSITyh/59SOexDkNxY2IpSx1cD6FrJUkqbKAbJ9PgqA18aAauQnaMpIUJ4Y +128iVH7H2AgrQPFNpgbRj7lIsn6Y9W6Uj5PA3jQHepypaT3S+F4HhcD15S7V2bi9olBmYv +2O+fKSNH47KwAAAIEA7uhpTBM9C6CXKoFu2FISKLaJ63gk0Z32ezyRyv2KH2ctJV1psuqd +MiaihieGg+6tXuth/EoMWteagDG+845G/BA4OOMV1vidAf2MhzmkO4XPRGJ3eFIaHhXl5i +mIhcDsJWQbZZ3lInWOyGgP2GmLt/1WlBVa/ueYfnkXeMmYEysAAACBAMkIKV6oPTmGCu8w +CxaFi27NPbGtVlfVUqxTLXUqb4KR6I3xdzNkX89dMUciY3ty0U6KHlyvuZ8NbHOS0DFRAj +kEUUUYbBzkLkM1RxzAWiy+BueGdxNZnPuFqi5qsgguHPhMe0+2H4TlQn9J3pRK5bM3SoK2 +j6FW0DcOmqu981bVAAAAGmxla3NlQERvbWluaWtzLU1CUC0yLmxvY2Fs +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/ssh-keys/test.pub b/tests/ssh-keys/test.pub new file mode 100644 index 00000000..ea23d368 --- /dev/null +++ b/tests/ssh-keys/test.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7nBiLlLEJNyYKFVZpLfgxhXbkYLR7KuE8u3niWYE0eYD7TzxrTfxrMEyzz9fFhwylwioT3ZQFe4xdF7bfSV5Xfq3RDDSff96D84/1y1II4PWkhKOwsVKLlROn11tSV49htiZuxGbXj7e+EZeU4tRzoCl907jCfXc1KL/NBWsS2XPXkZQ1aaAUo98Uc1OVAM5AoqkyA0p5TFt6rWR+sDhv/0/BnJpBxCfzhKo0A5hO7LxmqcLsDrJEkUt+erE67pNhSQ7dX9IfuiK+HTZsKAowJOLVHzPSovDP6+f7FFhvfKwPFTOoxKhnyssFeOesX3G/dSirRvpt9sqWaHLCyWTH diff --git a/tests/wait-postgres-docker.sh b/tests/wait-postgres-docker.sh index 1246ba4b..16d8b18a 100755 --- a/tests/wait-postgres-docker.sh +++ b/tests/wait-postgres-docker.sh @@ -21,3 +21,15 @@ until docker-compose -f "$COMPOSE_FILE" logs postgres | grep "ready to accept co printf "." sleep 1 done + +echo "Waiting for ssh to be up" +until docker-compose -f "$COMPOSE_FILE" logs ssh | grep "Running /usr/sbin/sshd" > /dev/null; do + i=$((i + 1)) + if [ $i -eq $TIMEOUT ]; then + echo + echo "Timeout while waiting for ssh to be up" + exit 1 + fi + printf "." + sleep 1 +done \ No newline at end of file