update ssh communicator to use new types

remove the legacy types and mapstructure from the ssh communicator
This commit is contained in:
James Bardin 2020-11-25 12:40:42 -05:00
parent 987df2003a
commit 5cc3fd60d9
4 changed files with 220 additions and 211 deletions

View File

@ -20,9 +20,12 @@ import (
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/communicator/remote"
"github.com/hashicorp/terraform/internal/legacy/terraform"
"github.com/hashicorp/terraform/provisioners"
"github.com/zclconf/go-cty/cty"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
_ "github.com/hashicorp/terraform/internal/logging"
)
const (
@ -84,8 +87,8 @@ func (e fatalError) FatalError() error {
}
// New creates a new communicator implementation over SSH.
func New(s *terraform.InstanceState) (*Communicator, error) {
connInfo, err := parseConnectionInfo(s)
func New(v cty.Value) (*Communicator, error) {
connInfo, err := parseConnectionInfo(v)
if err != nil {
return nil, err
}
@ -117,7 +120,7 @@ func New(s *terraform.InstanceState) (*Communicator, error) {
}
// Connect implementation of communicator.Communicator interface
func (c *Communicator) Connect(o terraform.UIOutput) (err error) {
func (c *Communicator) Connect(o provisioners.UIOutput) (err error) {
// Grab a lock so we can modify our internal attributes
c.lock.Lock()
defer c.lock.Unlock()

View File

@ -20,7 +20,7 @@ import (
"time"
"github.com/hashicorp/terraform/communicator/remote"
"github.com/hashicorp/terraform/internal/legacy/terraform"
"github.com/zclconf/go-cty/cty"
"golang.org/x/crypto/ssh"
)
@ -125,20 +125,16 @@ func TestNew_Invalid(t *testing.T) {
address := newMockLineServer(t, nil, testClientPublicKey)
parts := strings.Split(address, ":")
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "user",
"password": "i-am-invalid",
"host": parts[0],
"port": parts[1],
"timeout": "30s",
},
},
}
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"user": cty.StringVal("user"),
"password": cty.StringVal("i-am-invalid"),
"host": cty.StringVal(parts[0]),
"port": cty.StringVal(parts[1]),
"timeout": cty.StringVal("30s"),
})
c, err := New(r)
c, err := New(v)
if err != nil {
t.Fatalf("error creating communicator: %s", err)
}
@ -150,19 +146,15 @@ func TestNew_Invalid(t *testing.T) {
}
func TestNew_InvalidHost(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "user",
"password": "i-am-invalid",
"port": "22",
"timeout": "30s",
},
},
}
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"user": cty.StringVal("user"),
"password": cty.StringVal("i-am-invalid"),
"port": cty.StringVal("22"),
"timeout": cty.StringVal("30s"),
})
_, err := New(r)
_, err := New(v)
if err == nil {
t.Fatal("should have had an error creating communicator")
}
@ -172,20 +164,16 @@ func TestStart(t *testing.T) {
address := newMockLineServer(t, nil, testClientPublicKey)
parts := strings.Split(address, ":")
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "user",
"password": "pass",
"host": parts[0],
"port": parts[1],
"timeout": "30s",
},
},
}
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"user": cty.StringVal("user"),
"password": cty.StringVal("pass"),
"host": cty.StringVal(parts[0]),
"port": cty.StringVal(parts[1]),
"timeout": cty.StringVal("30s"),
})
c, err := New(r)
c, err := New(v)
if err != nil {
t.Fatalf("error creating communicator: %s", err)
}
@ -211,19 +199,15 @@ func TestKeepAlives(t *testing.T) {
address := newMockLineServer(t, nil, testClientPublicKey)
parts := strings.Split(address, ":")
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "user",
"password": "pass",
"host": parts[0],
"port": parts[1],
},
},
}
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"user": cty.StringVal("user"),
"password": cty.StringVal("pass"),
"host": cty.StringVal(parts[0]),
"port": cty.StringVal(parts[1]),
})
c, err := New(r)
c, err := New(v)
if err != nil {
t.Fatalf("error creating communicator: %s", err)
}
@ -261,19 +245,16 @@ func TestFailedKeepAlives(t *testing.T) {
address := newMockLineServer(t, nil, testClientPublicKey)
parts := strings.Split(address, ":")
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "user",
"password": "pass",
"host": parts[0],
"port": parts[1],
},
},
}
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"user": cty.StringVal("user"),
"password": cty.StringVal("pass"),
"host": cty.StringVal(parts[0]),
"port": cty.StringVal(parts[1]),
"timeout": cty.StringVal("30s"),
})
c, err := New(r)
c, err := New(v)
if err != nil {
t.Fatalf("error creating communicator: %s", err)
}
@ -296,20 +277,16 @@ func TestLostConnection(t *testing.T) {
address := newMockLineServer(t, nil, testClientPublicKey)
parts := strings.Split(address, ":")
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "user",
"password": "pass",
"host": parts[0],
"port": parts[1],
"timeout": "30s",
},
},
}
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"user": cty.StringVal("user"),
"password": cty.StringVal("pass"),
"host": cty.StringVal(parts[0]),
"port": cty.StringVal(parts[1]),
"timeout": cty.StringVal("30s"),
})
c, err := New(r)
c, err := New(v)
if err != nil {
t.Fatalf("error creating communicator: %s", err)
}
@ -586,19 +563,15 @@ func TestAccUploadFile(t *testing.T) {
t.Skip()
}
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": os.Getenv("USER"),
"host": "127.0.0.1",
"port": "22",
"timeout": "30s",
},
},
}
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"user": cty.StringVal(os.Getenv("USER")),
"host": cty.StringVal("127.0.0.1"),
"port": cty.StringVal("22"),
"timeout": cty.StringVal("30s"),
})
c, err := New(r)
c, err := New(v)
if err != nil {
t.Fatalf("error creating communicator: %s", err)
}
@ -634,19 +607,15 @@ func TestAccHugeUploadFile(t *testing.T) {
t.Skip()
}
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": os.Getenv("USER"),
"host": "127.0.0.1",
"port": "22",
"timeout": "30s",
},
},
}
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"host": cty.StringVal("127.0.0.1"),
"user": cty.StringVal(os.Getenv("USER")),
"port": cty.StringVal("22"),
"timeout": cty.StringVal("30s"),
})
c, err := New(r)
c, err := New(v)
if err != nil {
t.Fatalf("error creating communicator: %s", err)
}
@ -706,16 +675,13 @@ func TestScriptPath(t *testing.T) {
}
for _, tc := range cases {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"host": "127.0.0.1",
"script_path": tc.Input,
},
},
}
comm, err := New(r)
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"host": cty.StringVal("127.0.0.1"),
"script_path": cty.StringVal(tc.Input),
})
comm, err := New(v)
if err != nil {
t.Fatalf("err: %s", err)
}
@ -735,14 +701,10 @@ func TestScriptPath_randSeed(t *testing.T) {
// Pre GH-4186 fix, this value was the deterministic start the pseudorandom
// chain of unseeded math/rand values for Int31().
staticSeedPath := "/tmp/terraform_1298498081.sh"
c, err := New(&terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"host": "127.0.0.1",
},
},
})
c, err := New(cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"host": cty.StringVal("127.0.0.1"),
}))
if err != nil {
t.Fatalf("err: %s", err)
}

View File

@ -10,13 +10,13 @@ import (
"net"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/hashicorp/terraform/communicator/shared"
"github.com/hashicorp/terraform/internal/legacy/terraform"
"github.com/mitchellh/mapstructure"
sshagent "github.com/xanzy/ssh-agent"
"github.com/zclconf/go-cty/cty"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"golang.org/x/crypto/ssh/knownhosts"
@ -51,41 +51,104 @@ const (
type connectionInfo struct {
User string
Password string
PrivateKey string `mapstructure:"private_key"`
Certificate string `mapstructure:"certificate"`
PrivateKey string
Certificate string
Host string
HostKey string `mapstructure:"host_key"`
HostKey string
Port int
Agent bool
ScriptPath string
TargetPlatform string
Timeout string
ScriptPath string `mapstructure:"script_path"`
TimeoutVal time.Duration `mapstructure:"-"`
TargetPlatform string `mapstructure:"target_platform"`
TimeoutVal time.Duration
BastionUser string `mapstructure:"bastion_user"`
BastionPassword string `mapstructure:"bastion_password"`
BastionPrivateKey string `mapstructure:"bastion_private_key"`
BastionCertificate string `mapstructure:"bastion_certificate"`
BastionHost string `mapstructure:"bastion_host"`
BastionHostKey string `mapstructure:"bastion_host_key"`
BastionPort int `mapstructure:"bastion_port"`
BastionUser string
BastionPassword string
BastionPrivateKey string
BastionCertificate string
BastionHost string
BastionHostKey string
BastionPort int
AgentIdentity string `mapstructure:"agent_identity"`
AgentIdentity string
}
// parseConnectionInfo is used to convert the ConnInfo of the InstanceState into
// a ConnectionInfo struct
func parseConnectionInfo(s *terraform.InstanceState) (*connectionInfo, error) {
// decodeConnInfo decodes the given cty.Value using the same behavior as the
// lgeacy mapstructure decoder in order to preserve as much of the existing
// logic as possible for compatibility.
func decodeConnInfo(v cty.Value) (*connectionInfo, error) {
connInfo := &connectionInfo{}
decConf := &mapstructure.DecoderConfig{
WeaklyTypedInput: true,
Result: connInfo,
if v.IsNull() {
return connInfo, nil
}
dec, err := mapstructure.NewDecoder(decConf)
for k, v := range v.AsValueMap() {
if v.IsNull() {
continue
}
switch k {
case "user":
connInfo.User = v.AsString()
case "password":
connInfo.Password = v.AsString()
case "private_key":
connInfo.PrivateKey = v.AsString()
case "certificate":
connInfo.Certificate = v.AsString()
case "host":
connInfo.Host = v.AsString()
case "host_key":
connInfo.HostKey = v.AsString()
case "port":
p, err := strconv.Atoi(v.AsString())
if err != nil {
return nil, err
}
connInfo.Port = p
case "agent":
connInfo.Agent = v.True()
case "script_path":
connInfo.ScriptPath = v.AsString()
case "target_platform":
connInfo.TargetPlatform = v.AsString()
case "timeout":
connInfo.Timeout = v.AsString()
case "bastion_user":
connInfo.BastionUser = v.AsString()
case "bastion_password":
connInfo.BastionPassword = v.AsString()
case "bastion_private_key":
connInfo.BastionPrivateKey = v.AsString()
case "bastion_certificate":
connInfo.BastionCertificate = v.AsString()
case "bastion_host":
connInfo.BastionHost = v.AsString()
case "bastion_host_key":
connInfo.BastionHostKey = v.AsString()
case "bastion_port":
p, err := strconv.Atoi(v.AsString())
if err != nil {
return nil, err
}
connInfo.BastionPort = p
case "agent_identity":
connInfo.AgentIdentity = v.AsString()
}
}
return connInfo, nil
}
// parseConnectionInfo is used to convert the raw configuration into the
// *connectionInfo struct.
func parseConnectionInfo(v cty.Value) (*connectionInfo, error) {
v, err := shared.ConnectionBlockSupersetSchema.CoerceValue(v)
if err != nil {
return nil, err
}
if err := dec.Decode(s.Ephemeral.ConnInfo); err != nil {
connInfo, err := decodeConnInfo(v)
if err != nil {
return nil, err
}
@ -94,7 +157,8 @@ func parseConnectionInfo(s *terraform.InstanceState) (*connectionInfo, error) {
//
// And if SSH_AUTH_SOCK is not set, there's no agent to connect to, so we
// shouldn't try.
if s.Ephemeral.ConnInfo["agent"] == "" && os.Getenv("SSH_AUTH_SOCK") != "" {
agent := v.GetAttr("agent")
if agent.IsNull() && os.Getenv("SSH_AUTH_SOCK") != "" {
connInfo.Agent = true
}

View File

@ -3,28 +3,23 @@ package ssh
import (
"testing"
"github.com/hashicorp/terraform/internal/legacy/terraform"
"github.com/zclconf/go-cty/cty"
)
func TestProvisioner_connInfo(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "root",
"password": "supersecret",
"private_key": "someprivatekeycontents",
"certificate": "somecertificate",
"host": "127.0.0.1",
"port": "22",
"timeout": "30s",
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"user": cty.StringVal("root"),
"password": cty.StringVal("supersecret"),
"private_key": cty.StringVal("someprivatekeycontents"),
"certificate": cty.StringVal("somecertificate"),
"host": cty.StringVal("127.0.0.1"),
"port": cty.StringVal("22"),
"timeout": cty.StringVal("30s"),
"bastion_host": cty.StringVal("127.0.1.1"),
})
"bastion_host": "127.0.1.1",
},
},
}
conf, err := parseConnectionInfo(r)
conf, err := parseConnectionInfo(v)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -74,24 +69,18 @@ func TestProvisioner_connInfo(t *testing.T) {
}
func TestProvisioner_connInfoIpv6(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "root",
"password": "supersecret",
"private_key": "someprivatekeycontents",
"certificate": "somecertificate",
"host": "::1",
"port": "22",
"timeout": "30s",
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"user": cty.StringVal("root"),
"password": cty.StringVal("supersecret"),
"private_key": cty.StringVal("someprivatekeycontents"),
"host": cty.StringVal("::1"),
"port": cty.StringVal("22"),
"timeout": cty.StringVal("30s"),
"bastion_host": cty.StringVal("::1"),
})
"bastion_host": "::1",
},
},
}
conf, err := parseConnectionInfo(r)
conf, err := parseConnectionInfo(v)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -106,22 +95,18 @@ func TestProvisioner_connInfoIpv6(t *testing.T) {
}
func TestProvisioner_connInfoHostname(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "root",
"password": "supersecret",
"private_key": "someprivatekeycontents",
"host": "example.com",
"port": "22",
"timeout": "30s",
"bastion_host": "example.com",
},
},
}
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"user": cty.StringVal("root"),
"password": cty.StringVal("supersecret"),
"private_key": cty.StringVal("someprivatekeycontents"),
"host": cty.StringVal("example.com"),
"port": cty.StringVal("22"),
"timeout": cty.StringVal("30s"),
"bastion_host": cty.StringVal("example.com"),
})
conf, err := parseConnectionInfo(r)
conf, err := parseConnectionInfo(v)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -136,21 +121,16 @@ func TestProvisioner_connInfoHostname(t *testing.T) {
}
func TestProvisioner_connInfoEmptyHostname(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "root",
"password": "supersecret",
"private_key": "someprivatekeycontents",
"host": "",
"port": "22",
"timeout": "30s",
},
},
}
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"user": cty.StringVal("root"),
"password": cty.StringVal("supersecret"),
"private_key": cty.StringVal("someprivatekeycontents"),
"port": cty.StringVal("22"),
"timeout": cty.StringVal("30s"),
})
_, err := parseConnectionInfo(r)
_, err := parseConnectionInfo(v)
if err == nil {
t.Fatalf("bad: should not allow empty host")
}