diff --git a/communicator/shared/shared.go b/communicator/shared/shared.go new file mode 100644 index 000000000000..39cb16961a43 --- /dev/null +++ b/communicator/shared/shared.go @@ -0,0 +1,17 @@ +package shared + +import ( + "fmt" + "net" +) + +// IpFormat formats the IP correctly, so we don't provide IPv6 address in an IPv4 format during node communication. We return the ip parameter as is if it's an IPv4 address or a hostname. +func IpFormat(ip string) string { + ipObj := net.ParseIP(ip) + // Return the ip/host as is if it's either a hostname or an IPv4 address. + if ipObj == nil || ipObj.To4() != nil { + return ip + } + + return fmt.Sprintf("[%s]", ip) +} diff --git a/communicator/shared/shared_test.go b/communicator/shared/shared_test.go new file mode 100644 index 000000000000..575e5f78de35 --- /dev/null +++ b/communicator/shared/shared_test.go @@ -0,0 +1,26 @@ +package shared + +import ( + "testing" +) + +func TestIpFormatting_Ipv4(t *testing.T) { + formatted := IpFormat("127.0.0.1") + if formatted != "127.0.0.1" { + t.Fatal("expected", "127.0.0.1", "got", formatted) + } +} + +func TestIpFormatting_Hostname(t *testing.T) { + formatted := IpFormat("example.com") + if formatted != "example.com" { + t.Fatal("expected", "example.com", "got", formatted) + } +} + +func TestIpFormatting_Ipv6(t *testing.T) { + formatted := IpFormat("::1") + if formatted != "[::1]" { + t.Fatal("expected", "[::1]", "got", formatted) + } +} diff --git a/communicator/ssh/provisioner.go b/communicator/ssh/provisioner.go index 48eaafe388c5..07d50a051bfb 100644 --- a/communicator/ssh/provisioner.go +++ b/communicator/ssh/provisioner.go @@ -8,6 +8,7 @@ import ( "os" "time" + "github.com/hashicorp/terraform/communicator/shared" "github.com/hashicorp/terraform/helper/pathorcontents" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/mapstructure" @@ -84,6 +85,11 @@ func parseConnectionInfo(s *terraform.InstanceState) (*connectionInfo, error) { if connInfo.User == "" { connInfo.User = DefaultUser } + + // Format the host if needed. + // Needed for IPv6 support. + connInfo.Host = shared.IpFormat(connInfo.Host) + if connInfo.Port == 0 { connInfo.Port = DefaultPort } @@ -107,6 +113,10 @@ func parseConnectionInfo(s *terraform.InstanceState) (*connectionInfo, error) { // Default all bastion config attrs to their non-bastion counterparts if connInfo.BastionHost != "" { + // Format the bastion host if needed. + // Needed for IPv6 support. + connInfo.BastionHost = shared.IpFormat(connInfo.BastionHost) + if connInfo.BastionUser == "" { connInfo.BastionUser = connInfo.User } diff --git a/communicator/ssh/provisioner_test.go b/communicator/ssh/provisioner_test.go index aa029dad86df..051d8d34de79 100644 --- a/communicator/ssh/provisioner_test.go +++ b/communicator/ssh/provisioner_test.go @@ -66,6 +66,68 @@ 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", + "host": "::1", + "port": "22", + "timeout": "30s", + + "bastion_host": "::1", + }, + }, + } + + conf, err := parseConnectionInfo(r) + if err != nil { + t.Fatalf("err: %v", err) + } + + if conf.Host != "[::1]" { + t.Fatalf("bad: %v", conf) + } + + if conf.BastionHost != "[::1]" { + t.Fatalf("bad %v", conf) + } +} + +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", + }, + }, + } + + conf, err := parseConnectionInfo(r) + if err != nil { + t.Fatalf("err: %v", err) + } + + if conf.Host != "example.com" { + t.Fatalf("bad: %v", conf) + } + + if conf.BastionHost != "example.com" { + t.Fatalf("bad %v", conf) + } +} + func TestProvisioner_connInfoLegacy(t *testing.T) { r := &terraform.InstanceState{ Ephemeral: terraform.EphemeralState{ diff --git a/communicator/winrm/provisioner.go b/communicator/winrm/provisioner.go index d1562998cd42..2dab1e97d4ac 100644 --- a/communicator/winrm/provisioner.go +++ b/communicator/winrm/provisioner.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/hashicorp/terraform/communicator/shared" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/mapstructure" ) @@ -72,6 +73,11 @@ func parseConnectionInfo(s *terraform.InstanceState) (*connectionInfo, error) { if connInfo.User == "" { connInfo.User = DefaultUser } + + // Format the host if needed. + // Needed for IPv6 support. + connInfo.Host = shared.IpFormat(connInfo.Host) + if connInfo.Port == 0 { connInfo.Port = DefaultPort } diff --git a/communicator/winrm/provisioner_test.go b/communicator/winrm/provisioner_test.go index 9a271ae59ef9..9ed6f095d125 100644 --- a/communicator/winrm/provisioner_test.go +++ b/communicator/winrm/provisioner_test.go @@ -49,6 +49,92 @@ func TestProvisioner_connInfo(t *testing.T) { } } +func TestProvisioner_connInfoIpv6(t *testing.T) { + r := &terraform.InstanceState{ + Ephemeral: terraform.EphemeralState{ + ConnInfo: map[string]string{ + "type": "winrm", + "user": "Administrator", + "password": "supersecret", + "host": "::1", + "port": "5985", + "https": "true", + "timeout": "30s", + }, + }, + } + + conf, err := parseConnectionInfo(r) + if err != nil { + t.Fatalf("err: %v", err) + } + + if conf.User != "Administrator" { + t.Fatalf("expected: %v: got: %v", "Administrator", conf) + } + if conf.Password != "supersecret" { + t.Fatalf("expected: %v: got: %v", "supersecret", conf) + } + if conf.Host != "[::1]" { + t.Fatalf("expected: %v: got: %v", "[::1]", conf) + } + if conf.Port != 5985 { + t.Fatalf("expected: %v: got: %v", 5985, conf) + } + if conf.HTTPS != true { + t.Fatalf("expected: %v: got: %v", true, conf) + } + if conf.Timeout != "30s" { + t.Fatalf("expected: %v: got: %v", "30s", conf) + } + if conf.ScriptPath != DefaultScriptPath { + t.Fatalf("expected: %v: got: %v", DefaultScriptPath, conf) + } +} + +func TestProvisioner_connInfoHostname(t *testing.T) { + r := &terraform.InstanceState{ + Ephemeral: terraform.EphemeralState{ + ConnInfo: map[string]string{ + "type": "winrm", + "user": "Administrator", + "password": "supersecret", + "host": "example.com", + "port": "5985", + "https": "true", + "timeout": "30s", + }, + }, + } + + conf, err := parseConnectionInfo(r) + if err != nil { + t.Fatalf("err: %v", err) + } + + if conf.User != "Administrator" { + t.Fatalf("expected: %v: got: %v", "Administrator", conf) + } + if conf.Password != "supersecret" { + t.Fatalf("expected: %v: got: %v", "supersecret", conf) + } + if conf.Host != "example.com" { + t.Fatalf("expected: %v: got: %v", "example.com", conf) + } + if conf.Port != 5985 { + t.Fatalf("expected: %v: got: %v", 5985, conf) + } + if conf.HTTPS != true { + t.Fatalf("expected: %v: got: %v", true, conf) + } + if conf.Timeout != "30s" { + t.Fatalf("expected: %v: got: %v", "30s", conf) + } + if conf.ScriptPath != DefaultScriptPath { + t.Fatalf("expected: %v: got: %v", DefaultScriptPath, conf) + } +} + func TestProvisioner_formatDuration(t *testing.T) { cases := map[string]struct { InstanceState *terraform.InstanceState