diff --git a/communicator/shared/shared.go b/communicator/shared/shared.go new file mode 100644 index 000000000..39cb16961 --- /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 000000000..575e5f78d --- /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 48eaafe38..07d50a051 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 aa029dad8..051d8d34d 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 d1562998c..2dab1e97d 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 9a271ae59..9ed6f095d 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